N+1問題とは?
N+1問題とは、データベースからデータを取り出す際に、大量のSQLが発行されてパフォーマンスが低下してしまう問題のことです。
N+1問題の具体例
railsではallメソッドやfindメソッドを使ってデータベースからデータを取得しています。
ターミナルのログを見ると実際には下のようにその都度SQLが実行されています。
Product Load (2.7ms) SELECT `products`.* FROM `products`
usersテーブル
id | name |
---|---|
1 | 山田 |
2 | 新井 |
3 | 田中 |
4 | 北川 |
productsテーブル
id | product | group_id |
---|---|---|
1 | カレー | 2 |
2 | 魚 | 1 |
3 | 焼肉 | 3 |
4 | 刺身 | 1 |
「1人のuserは複数のproductsを持つ関係なので、Userモデルにhas_manyメソッドを定義し、Productモデルにはbelongs_toメソッドを定義します。
UserモデルとProductモデルにアソシエーション定義
# User.rb
class User < ActiveRecord::Base
has_many :Products
end
# Product.rb
class Product < ActiveRecord::Base
belongs_to :User
end
全ての所有の商品一覧」をviewで表示したい場合に、controller側で全てのuserをallメソッドで取得し、view側で飼い主の持つproductsをアソシエーションによって下記の様に記述する事が出来ます。
# controller
@users = User.all
# view
@users.each do |user|
user.products.each do |product|
product.name
end
end
「User.all」のコードが実行されると、「usersテーブルからusersテーブルの全てのカラム」が取得。
このSQLによって、usersテーブルに1回のアクセス。
次にveiw側。SQLをみると、productsテーブルに対して、4回のアクセスが行われている。
@groups.each do |group|
group.products.each do |product|
cat.name
end
end
# このコードが4回のSQL文を発行
SELECT `products`.* FROM `products` WHERE `products`.`group_id` = 1
SELECT `products`.* FROM `products` WHERE `products`.`group_id` = 2
SELECT `products`.* FROM `products` WHERE `products`.`group_id` = 3
SELECT `products`.* FROM `products` WHERE `products`.`group_id` = 4
eachメソッドで@groupsが持つusersテーブルから全てのレコードを一つずつgroupに入れている。
SQLが「usersテーブルへのアクセスが1回 」に対して「productsテーブルへのアクセスがgroupsテーブルのレコードの数(4回)」発行
このようにアクセス1回に対して、関連するテーブルがN回発行されている1+Nの状況を「N+1問題」と言う。
。
N+1問題の対処法
includesメソッド
includesメソッドの使用例 -->
@users = User.includes(:user)
@users = User.allで取得していた箇所を@users = User.includes(:products)に変更します
@users = User.includes(:products) # User.allから変更
# 発行される2つのSQL
SELECT `users`.* FROM `users`
SELECT `products`.* FROM `products` WHERE `products`.`group_id` IN (1, 2, 3, 4)
2つのSQLが発行されました。1行目は、usersテーブルの全てのレコードを取得するSQL文です。
2行目は、productsテーブルからWHERE句で指定した条件にマッチするレコードを取得しています。
まとめ
includesメソッドを使わない場合は、関連するuser_idカラムの値を1つずつ指定して取得していたのでproductsテーブルに4回のアクセスが必要でしたが、IN句でカラムの値をまとめて指定した事によって1回で取得出来るようになりました。includesメソッドを使ってレコードをまとめて取得させる事によって必要以上のSQLを発行する事なく済み,パフォーマンス向上に繋がる