概要
RailsでN+1問題を解決する際によく使われるpreloadとeager_loadの違いと個人的な使い分け方針をまとめました。
背景
先日実務で扱っているアプリケーションにて、メモリ使用率が急上昇しプロセスがダウンしてしまうトラブルがありました。原因は、ActiveRecord関連の処理でJOINやLEFT_JOINを複数回重ねたSQLが発行されており、その結果膨大な件数のレコードがRailsに返却され、それらのオブジェクトがメモリ上に展開されてしまったというものでした。
上記の経験から、普段何気なく使っているpreloadやeager_loadも場合によってはメモリを圧迫しかねないのではと思い、改めてこれらの処理が何をしているのかを理解し利用方針を整理することにしました。
詳細
仕組みの違い
preload, eager_loadいずれも関連先のレコードをキャッシュしてくれるものですが、その仕組みには以下のような違いがあります。
| preload | eager_load | |
|---|---|---|
| キャッシュ方法 | 関連先毎にクエリを発行 | 関連先をLEFT OUTER JOINで結合 |
| SQL発行回数 | 結合元で1回+関連先数分 | 1回 |
| 結合先での絞り込み | 不可 | 可能 |
-
Article.preload(:comments)で実行されるSQL
-- 1. articlesテーブルから全ての記事を取得
SELECT * FROM articles;
-- 2. commentsテーブルから必要なコメントを一度のクエリで取得
SELECT * FROM comments WHERE article_id IN (1, 2, 3, ...);
-
Article.eager_load(:comments)で実行されるSQL
-- articlesテーブルとcommentsテーブルを結合して、全ての記事とコメントを一度のクエリで取得
SELECT articles.*, comments.* FROM articles
LEFT OUTER JOIN comments ON comments.article_id = articles.id;
使い分け
いずれも関連先のデータをキャッシュするため、関連先のレコード数が膨大な場合はメモリを大量に消費してしまう可能性があり注意が必要です。以下に個人的な使い分け方針をまとめます。
①関連先で絞り込みが必要 → eager_load
preloadは関連先での絞り込みができません。
なお関連先のキャッシュは不要で単に関連先で絞り込みを行いたいだけの場合は、補足に記載のjoinsを使いましょう。(メモリが節約できる。)
②複数の関連先をキャッシュしたい → preload(関連先のレコード数にもよる)
もしeager_load(:comments, :author)のように、eager_loadで複数の関連先をキャッシュしようとした場合、LEFT OUTER JOINが複数回実行されるため、結合後のテーブルのレコード数が膨大になり、パフォーマンスの低下やメモリを大幅に消費する恐れがあります。複数の関連先をキャッシュする場合は、preloadで関連先の数分クエリを発行したほうが高速かつメモリの消費も抑えられる可能性が高いです。
③その他の場合 → まずはpreload、遅い場合はeager_load
関連先での絞り込み不要で、1つの関連先をキャッシュする場合は、まずはpreloadを試してみることをお勧めします。この場合はpreloadでも発行されるSQLは2文であるため、eager_loadの場合(LEFT OUTER JOINS)と比較してもそこまで性能の差は出ないのではと考えています。もしpreloadで遅いと感じる場合はeager_loadを試してみる、といった流れでもよいのではないでしょうか。
本番運用開始から日数が経過するにつれてDBのレコード数はどんどん増えていきます。
リリース当初はeager_load(LEFT OUTER JOIN)でも問題なかったが、ある時期からパフォーマンスの低下が謙虚になった、というケースも考えられなくはないためpreloadを選択したほうが事故は起きにくいのかなという気がしています。
補足
- includesについて
includesは状況によってpreloadとして働いたり、eager_loadとして働いたりと予期せぬ挙動を取りかねないため、直接preloadまたはeager_loadを使うことをお勧めします。 - joinsについて
joinsは関連先の項目で絞り込みをしたい場合に使われます。preloadやeager_loadと異なり関連先をキャッシュしません。
余談
本題と少し逸れますが、複雑なSQLを1つ実行するよりも、単純なSQLを複数回実行する方がパフォーマンス、リソース面で優れている場合も多いようです。これについては、SQLアンチパターンという書籍で、「スパゲティクエリ」という章でも述べられています。