掲示板実装① N+1問題を解決する
掲示板実装時、N+1問題を解決する必要が生じたので、解決方法をまとめました。
boards_controller内の通常のコード
def index @boards = Board.all end
このような記述でも、indexビューは正常に表示されるが、サーバーログを見てみると何十回にも渡って読み込みが行われているのがわかる。
Started GET "/boards/index" for ::1 at 2020-10-20 12:47:36 +0900 Processing by BoardsController#index as HTML Rendering boards/index.html.erb within layouts/application Board Load (0.5ms) SELECT "boards".* FROM "boards" ↳ app/views/boards/index.html.erb:17 User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 2], ["LIMIT", 1]] ↳ app/views/boards/_board.html.erb:17 CACHE User Load (0.0ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 2], ["LIMIT", 1]] ↳ app/views/boards/_board.html.erb:17 CACHE User Load (0.0ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 2], ["LIMIT", 1]] ↳ app/views/boards/_board.html.erb:17 User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 4], ["LIMIT", 1]] ↳ app/views/boards/_board.html.erb:17 CACHE User Load (0.0ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 4], ["LIMIT", 1]] ↳ app/views/boards/_board.html.erb:17 User Load (0.3ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 3], ["LIMIT", 1]] ↳ app/views/boards/_board.html.erb:17 User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 7], ["LIMIT", 1]] ↳ app/views/boards/_board.html.erb:17 . . . Completed 200 OK in 279ms (Views: 275.5ms | ActiveRecord: 2.7ms)
これは、
1、最初のindexへのgetリクエストでBoardテーブルの全てのデータを取得 2、レンダリングごとに、一回一回Userテーブルから該当するuser_idのデータを取得する
という処理が掲示板の数だけ行われているため。
そのため、どうしても処理が遅くなってしまう。
最初の1回で全データを取得し、N個の掲示板1つ1つについてUserテーブルからデータを取得しているので、「N+1問題」と呼ぶ。 (個人的には、1+N問題と書くほうがしっくりくる気がする。)
この処理を可能な限り減らすために、最初の1回で関連づけされているUsersテーブルのデータも一気に取得する
def index @boards = Board.all.includes(:user) end
Includeを追記することで、関連付けされているUsersテーブルのデータも取得することができる。 これにより、一回一回Usersテーブルにアクセスする必要がなくなる。
変更後のサーバーログ
Started GET "/boards/index" for ::1 at 2020-10-20 12:49:38 +0900 Processing by BoardsController#index as HTML Rendering boards/index.html.erb within layouts/application Board Load (0.4ms) SELECT "boards".* FROM "boards" ORDER BY "boards"."created_at" DESC ↳ app/views/boards/index.html.erb:17 User Load (0.4ms) SELECT "users".* FROM "users" WHERE "users"."id" IN (?, ?, ?, ?, ?, ?, ?) [["id", 3], ["id", 2], ["id", 6], ["i d", 1], ["id", 5], ["id", 7], ["id", 4]] ↳ app/views/boards/index.html.erb:17 Rendered collection of boards/_board.html.erb [33 times] (6.7ms) Rendered boards/index.html.erb within layouts/application (51.9ms) User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 13], ["LIMIT", 1]] ↳ app/views/layouts/application.html.erb:13 Rendered shared/_header.html.erb (5.9ms) Rendered shared/_flash_message.html.erb (1.0ms) Rendered shared/_footer.html.erb (0.7ms) Completed 200 OK in 162ms (Views: 154.3ms | ActiveRecord: 2.8ms)
上のログを見てもわかるように、ログも一気に減り処理にかかる時間も約1/2に短縮されたことがわかる。