0
0

More than 3 years have passed since last update.

【Rails】N+1問題についてのまとめ

Posted at

N+1問題とは?

N+1問題とは、データベースからデータを取り出す際に、大量のSQLが発行されてパフォーマンスが低下してしまう問題のことです。

N+1問題の具体例

railsではallメソッドやfindメソッドを使ってデータベースからデータを取得しています。
ターミナルのログを見ると実際には下のようにその都度SQLが実行されています。

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モデルと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をアソシエーションによって下記の様に記述する事が出来ます。

N+1問題が起きてしまうコードを確認

# 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回のアクセスが行われている。

N+1問題が起きてしまうコード
@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メソッド
includesメソッドの使用例 -->
@users = User.includes(:user)
@users = User.allで取得していた箇所を@users = User.includes(:products)に変更します
includesメソッド
@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を発行する事なく済み,パフォーマンス向上に繋がる:blush:

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0