LoginSignup
24
27

More than 3 years have passed since last update.

n+1問題とincludesメソッド

Last updated at Posted at 2019-06-05

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メソッドを使用することをお勧めする。
                              以上

24
27
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
24
27