Rails: where.firstとfind_byの違いを知る(翻訳)
Active RecordのようなORM(Object-Relational Mapper)でSQLを生成することには多くのメリットがあります。明確で再利用しやすい抽象化を手に入れられるので、時間も節約でき、可読性も向上します。
ただし、ORMの便利な抽象化機能そのものが思わぬ結果をもたらすこともあります。
データベースサーバー上で実行される「実際の」SQLはフレームワークが生成するので、気をつけておかないとたちまちパフォーマンスが低下してしまいます。
私たちは最近CoverageBookの案件でまさにこの問題を踏みました。
以下のように書くのではなく
where条件に続けてfirstを書く。
User.where(email: "andy@goodscary.com").first
訳注
この書き方はRuboCop RailsのFindByでも警告されます。
以下のように書くこと
find_byを使う。
User.find_by(email: "andy@goodscary.com")
User.find_by_email("andy@goodscary.com")
訳注
2番目のfind_by_emailのような動的なfind_by_*メソッドによる書き方は古い方法であり、RuboCop RailsのDynamicFindByでも警告されます(ただしRails 7でも一応使えます)。
そうする理由
これは、ORM(およびその周辺)に邪魔されて思わぬパフォーマンス問題をひきおこす事例のひとつです。
.whereのスコープには、主キーに対する暗黙のORDERスコープが隠れており、一見しただけではわかりません。
User.where(email: "andy@goodscary.com")
# SELECT "users".*
# FROM "users"
# WHERE "users"."email" = "andy@goodscary.com"
User.where(email: "andy@goodscary.com").first
# SELECT "users".*
# FROM "users"
# WHERE "users"."email" = "andy@goodscary.com"
# ORDER BY "users"."id" ASC
# LIMIT 1
User.find_by(email: "andy@goodscary.com")
# SELECT "users".*
# FROM "users"
# WHERE "users"."email" = "andy@goodscary.com"
# LIMIT 1
この複雑なケースでは、データベースにインデックスがあっても無力でした。クエリでインデックスが使われていたのですが、.where().firstを使っていたのが原因で、ソート順を確定するためにインデックスを用いないスキャンが意図せず発生し、パフォーマンスが大きく低下してしまいました。
さらに書き込みが1秒あたり数千件に達しており、たった1件のレコードを取り出すだけでソートが発生していたので、データベースが極めて強力であったにもかかわらずこの問題が発生していました。
クエリを実行中の.find_byや.where().firstの結果に対して.to_sqlを呼び出すことができず、生成されたSQLを正確に知るにはログ出力に頼るしかなかったので、この問題のデバッグに手こずりました。
一見同じようなことをやっていそうなメソッドであっても、Active RecordがどんなSQLを生成しているかを正確に知っておくのは「とても」重要です。
そうしない理由があるとすれば
負荷の少ない小規模なテーブルであれば、where().firstのパフォーマンス低下は無視できるでしょう。
概要
原著者の許諾を得て翻訳・公開いたします。
日本語タイトルは内容に即したものにしました。
参考: 週刊Railsウォッチ20220328
where.firstとfind_by