N+1問題
- 複数テーブルに跨る情報を表示する際などに、ループ処理の中で都度SQLを発行してしまいパフォーマンスが低下してしまう問題。
- SQLクエリが 「データ量N + 1回 」走ってしまうことからこのように名付けられている
- 以下をかなり参考にさせていただきました。
N+1問題が発生しうる具体例
- Qiitaのような記事投稿を例にとると...
- 記事の表示
- 記事に対するコメントの表示
- フォロー・フォロワーの表示
などがあります。
パターン
- 1 対 N パターン
- N 対 1 対 N パターン(ちょっとムズいのでまずは
1 対 Nパターン
から
1 対 Nパターン例
引続きQiitaライクなサービスを想像してください。
User
とArticle
の2つのモデル間で以下のようなアソシエーションが組まれることになるのが一般的。
- Userは複数のArticle(記事)を持つ
- Article(記事)はUserに属する
models/user.by
class User < ActiveRecord::Base
has_many :articles
end
models/article.rb
class Article < ActiveRecord::Base
belongs_to :user
end
この状態で全記事のタイトルと書いた人の一覧画面を表示しようとすると以下のような実装となる。
article_controller.rb
class ArticlesController < ApplicationController
def index
@articles = Article.limit(10)#今回は10人取得しとく
end
end
articles/index.html.slim
- @articles.each do |article|
= article.title
= article.user.name
こんな感じにするとActiveRecordの機能により以下のようなSQLが発行される
# Article.all の実行で記事を取得
SELECT 'articles'.* FROM 'articles'
# そしてarticlesのuser.name を10回取得するので以下のように10回SQLを発行することになる
SELECT 'users'.* FROM 'users' WHERE 'users'.'id' = 1 LIMIT 1
SELECT 'users'.* FROM 'users' WHERE 'users'.'id' = 2 LIMIT 1
SELECT 'users'.* FROM 'users' WHERE 'users'.'id' = 3 LIMIT 1
.
.
.
省略
SELECT 'users'.* FROM 'users' WHERE 'users'.'id' = 10 LIMIT 1
このようにarticle.user.name
をN(10)人取り出そうとN + 1回のSQLを発行しパフォーマンスが落ちる原因となる。
蛇足
※この記事を書くまでアソシエーションによって参照できるデータは親→子
の一方通行だと思ってた。。。両方可能なのね。。。恥
- アソシエーション (modelの関連付け)に関してはこちら
includesで関連付けを一括読み込みする
結論から
articles_controller.rb
class ArticlesController < ApplicationController
def index
@articles = Article.includes(:user).limit(10)#includesを追加
end
こうすることで、関連付けを一括で読み込んでくれて発行されるSQLがこうなるらしい。
# Article.all の実行で記事を取得
SELECT 'articles'.* FROM 'articles'
# 一括で読み込んでくれる
SELECT addresses.* FROM addresses WHERE WHERE `users`.`id` IN (1,2,3,4,5,6,7,8,9,10))
N対1対Nパターン
今回の場合はユーザが記事とコメントをhas_manyしている場合がありそう。
先ほどのuserモデル
に追加する形で以下のようにする。
models/user.rb
class User < ActiveRecord::Base
has_many :articles
has_many :comments
end
models/comment.rb
class Comment < ActiveRecord::Base
belongs_to :user
end
articles.index.html.slim
- @articles.each do |article|
= article.title
= article.user.name
#記事に対するコメントは複数があり得るため、`each`を入れ子にして表示している。
- article.user.comments.each do |comment|
= comment.content
登場人物が3人になったためarticle_controller側を変更しないとN+1問題が再び襲来する
以下のように変更する。
articles_controller.rb
class ArticlesController < ApplicationController
def index
@articles = Article.all.includes(user: :comments).limit(10)#commentsを追加
end
これでN+1問題は解決する。