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