前回の記事で、通常の ActiveRecord のクエリではパフォーマンスがよくないという場合に、うまくARELのよさを生かしつつ find_by_sql で高速なクエリを実行するための方法を紹介しました。
to_sql と find_by_sql でクエリを高速化する方法としてもうひとつ適用例があります。
本題に入る前に ActiveRecord が持っているあるパフォーマンス上の問題を紹介します。もしかすると最近の rails では改善されているのかもしれませんが、確認せずに書いてしまいます。その問題とは、joins を使ったテーブルに対しては eager loading が行われない、というものです。たとえば
users = User.joins(:emails).where("emails.address like '%@gmail.com'").includes(:emails)
のようにして、gmail のアカウントを持つユーザをとってきたとします。このクエリでは includes によって emails を eager loading するよう指示していることに注意してください。
さて、先頭のユーザが softbank のアドレスも持っているかどうかを調べたいとしましょう。
users.first.emails.any?{|e| e.address =~ /@softbank.ne.jp\Z/}
この時、users.first.emails にアクセスしたときにどうなるでしょうか?includes(:emails) を指定しているのだから eager loading された結果の emails がすでに存在するはずで、ここではクエリは発生しないはずです。ですが残念ながらそうはなりません。ここで select * from emails where user_id = xxx というクエリが発生してしまいます。いわゆる1+n問題が発生してしまうのです。しかもこれを回避するうまい方法というのもありません。preload_association がまだ生きていたころに association を強制注射してみましたが、eager loading のクエリは実行されるものの、モデルはセットされずに結局 lazy load が発生しました。
この問題に対してはいつも以下のように対処しています。
まず、前回の記事で説明したような方法を使って、select + to_sql でモデルの id のみを取得します。
ids = User.connection.select_rows( User.joins(:emails).where(...).select("users.id").to_sql )
このように、connection.select_rows を使うと id の配列が得られます。(正確には配列の配列だが)。前回は find_by_sql を使いましたが、モデルが不要な場合、select_rows などの下位APIを使って配列など生データのみ取得したほうがはるかに高速です。
そしてその後idだけをつかって
User.where(:id => ids).includes(:emails)
とするときれいなモデルが得られます。
前の記事で、ビジネスロジック上意味のある絞り込み条件(activatedなど)を定義することで再利用性が高まるということを書きましたが、これはここでも適用されます。そのような scope は最初の id を引っ張るところにうまく使い回しできて、実際に必要なデータ(カラムや関連)を取ってくるところは
二つ目のSQLで調整する、という役割分担になります。
0 件のコメント:
コメントを投稿