プログラミング学習 備忘録

Railsを学習していく上での技術メモ。学んだことや解決したエラーなどを記録していきます。

掲示板実装① 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に短縮されたことがわかる。