LoginSignup
6
10

More than 3 years have passed since last update.

rails N+1問題の対策(includes)

Last updated at Posted at 2019-08-20

N+1問題

N+1問題が発生しうる具体例

  • Qiitaのような記事投稿を例にとると...
    • 記事の表示
    • 記事に対するコメントの表示
    • フォロー・フォロワーの表示

などがあります。

パターン

  • 1 対 N パターン
  • N 対 1 対 N パターン(ちょっとムズいのでまずは1 対 Nパターンから

1 対 Nパターン例

引続きQiitaライクなサービスを想像してください。
UserArticleの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問題は解決する。

6
10
2

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
6
10