画面表示が遅いのは N+1問題が原因かも
N+1問題とは
DBからデータを取得する際に、都度SQLを発行してしまい、大量のSQLが発行されてパフォーマンスが悪くなってしまうことです。
例えるなら、ECサイトで10個の商品を注文した際に、1回で1つの段ボールに入って全ての商品が届くのが理想ですよね。
これが、1つずつ別の業者が届けにきて、計10個の段ボールが別々に届くようなイメージです。
見分け方
ではN+1問題をどのように見分けるか。
それはログを見ればわかります。
例としてターミナルの画像で紹介します。
赤枠で囲ってあるように、同じ内容のリクエスト(SQL)がいくつも発行されていることがわかります。
画面表示するレコードの件数が多いと、このリクエストの数もそれにつれて増えていき、10秒待たないと画面表示されないといった事象が発生します。
これが N+1問題です。
本来であれば、同じ内容のリクエストは1つにまとめることができる(ダンボールで商品を1つにできるのと同じです)ので、その方法を解説していきます!
解決手順
今回は、BooksController のindex、つまり一覧ページで N+1問題の解決をしていきます。
※ページングがあり、取得件数を5件で統一しています。
Book モデルと User モデルのリレーションとしては以下の通りです。
N+1問題が起きている状態
まずは N+1問題が起きている状態です。
さきほど紹介したように、内容が同じリクエストが複数あります。(赤枠部分)
この時のコントローラーの記述は以下の通りです。
Bookモデルから5件取得する記述になっています。
一見すると問題なさそうで、確かにコントローラー単体で見れば問題はありません。
しかし、N+1問題になる直接的な原因はView側にあるんです。
Viewファイルの該当部分を抜粋したのが以下のコードです。
“@books” をループで回しており、”a_book.user.id” の箇所で初めて User モデルを呼び出しています。
コントローラー側では Book モデルのレコードのみ使う想定でいるため、View側で必要になったものはその都度リクエストを発行して取得しなくてはなりません。
このように、Viewとコントローラーで統率が取れてないと N+1問題が発生する要員となります。
※逆に言えば、View側で Book モデルの内容のみ出力する(Userの内容を消す)ようにすると、N+1問題も無くなります。
解決後
結論から言うと、View側のコードは一切変更せずに、コントローラーの記述を少しだけ変えて、N+1問題を解決することができます。
対処後は以下の通り、同じリクエストが複数回されていた箇所が、1回になっています。
コントローラーで変更したのは、”includes(:user)”を追加したのみで、最終的に以下のようなコードになりました。
この “includes” というのは、モデルを結合してくれるもので、SQLで言うところのJOINです。
“join(:user)” という書き方でも大丈夫です。
“includes” によって結合されているため、View側でループする対象となる “@books” の中身は Book と User の両方が入っています。
このため、その都度 User の内容をDBから取得する必要が無くなり、リクエストの数が減るという仕組みです。
効果測定
N+1問題が解消すると、画面表示に劇的な改善をもたらします。
今回のケースで効果を見てみましょう。
5件で約6msの差があるため、単純に考えると、10件の表示では約12msの差となります。(恐らくもう少し差は広がります)
一般的にWebページの表示時間が0.1秒でも遅くなると、人間でも少し違和感を覚えるほどのレベルなので、100件ほどのデータ表示をする場合は死活問題となってきそうなのが分かります。
コメント