1. n+1問題について
n+1問題とは?
n+1問題とは、データベースからデータを取り出す際、大量のSQLが実行されて動作が重くなるという問題をいう。
大量のSQLが実行されてしまう理由は、テーブル間にアソシエーションが組まれているからである。具体的にいうと、一覧に表示するデータを取得するため、SELECTを1回実行すると、そのテーブルとアソシエーションが組まれている他のテーブルのデータにおいても、同時にSELECTが実行される。したがって、1回のSELECTでn回のSELECTが実行されてしまい、大量のSQLが実行されてしまうのである。
よって、テーブル間をbelongs_toやhas_manyでアソシエーションを組む際は、n+1問題を念頭に置かなければならない。
また、私見としては、n+1問題というよりも1+n問題と解釈した方がスッキリする。
n+1問題の具体例
モデル
# 都道府県.
class Prefecture < ActiveRecord::Base
end
# 店舗.
class Shop < ActiveRecord::Base
belongs_to :prefecture
end
コントローラ
class ShopsController < ApplicationController
def index
@shops = Shop.order(:id)
end
end
ビュー
<h1>店舗一覧<h1>
<% @shops.each do |shop| %>
<div><%= shop.name %> (<%= shop.prefecture.name %>)</div>
<% end %>
以上のプログラムを実行した後、ログファイルを覗いてみると、以下のようにprefectures (都道府県テーブル) へのSELECTが大量に出力されることになる。
Shop Load (0.7ms) SELECT "shops".* FROM "shops" ORDER BY id LIMIT 20
Prefecture Load (0.3ms) SELECT "prefectures".* FROM "prefectures" WHERE "prefectures"."id" = 73 LIMIT 1
CACHE (0.0ms) SELECT "prefectures".* FROM "prefectures" WHERE "prefectures"."id" = 73 LIMIT 1
Prefecture Load (0.4ms) SELECT "prefectures".* FROM "prefectures" WHERE "prefectures"."id" = 126 LIMIT 1
Prefecture Load (0.2ms) SELECT "prefectures".* FROM "prefectures" WHERE "prefectures"."id" = 36 LIMIT 1
Prefecture Load (0.2ms) SELECT "prefectures".* FROM "prefectures" WHERE "prefectures"."id" = 26 LIMIT 1
Prefecture Load (0.2ms) SELECT "prefectures".* FROM "prefectures" WHERE "prefectures"."id" = 96 LIMIT 1
Prefecture Load (0.2ms) SELECT "prefectures".* FROM "prefectures" WHERE "prefectures"."id" = 52 LIMIT 1
Prefecture Load (0.2ms) SELECT "prefectures".* FROM "prefectures" WHERE "prefectures"."id" = 34 LIMIT 1
Prefecture Load (0.2ms) SELECT "prefectures".* FROM "prefectures" WHERE "prefectures"."id" = 47 LIMIT 1
Prefecture Load (0.2ms) SELECT "prefectures".* FROM "prefectures" WHERE "prefectures"."id" = 56 LIMIT 1
CACHE (0.0ms) SELECT "prefectures".* FROM "prefectures" WHERE "prefectures"."id" = 26 LIMIT 1
Prefecture Load (0.2ms) SELECT "prefectures".* FROM "prefectures" WHERE "prefectures"."id" = 12 LIMIT 1
Prefecture Load (0.2ms) SELECT "prefectures".* FROM "prefectures" WHERE "prefectures"."id" = 70 LIMIT 1
Prefecture Load (0.2ms) SELECT "prefectures".* FROM "prefectures" WHERE "prefectures"."id" = 31 LIMIT 1
Prefecture Load (0.1ms) SELECT "prefectures".* FROM "prefectures" WHERE "prefectures"."id" = 11 LIMIT 1
Prefecture Load (0.2ms) SELECT "prefectures".* FROM "prefectures" WHERE "prefectures"."id" = 109 LIMIT 1
CACHE (0.0ms) SELECT "prefectures".* FROM "prefectures" WHERE "prefectures"."id" = 34 LIMIT 1
Prefecture Load (0.2ms) SELECT "prefectures".* FROM "prefectures" WHERE "prefectures"."id" = 107 LIMIT 1
Prefecture Load (0.2ms) SELECT "prefectures".* FROM "prefectures" WHERE "prefectures"."id" = 114 LIMIT 1
Prefecture Load (0.1ms) SELECT "prefectures".* FROM "prefectures" WHERE "prefectures"."id" = 24 LIMIT 1
データベース上のデータが多ければ、多いほど出力されるデータも多くなる。
2. includesメソッドについて
includesメソッドとは、関連づいたモデルを先に取得するメソッドをいう。
includesメソッドは、n+1問題の対処法に一つとして用いられる。
@tweets = Tweet.includes(:user)
このように「includes(:モデル名)」と指定する。
こうすることで、tweetsモデルからデータを取得するときに、関連するusersモデルのデータもまとめて取得してくれる。
そのため、eachメソッドで一つ一つ表示する際も、すでに表示させるデータを全て取得しているので、その都度SQLを実行する必要はなくなり、n+1問題が生じなくなる。一覧表示をさせたい際、モデル間において、アソシエーションが組まれている場合は、includeメソッドを使用することをお勧めする。
以上