プログラミングマガジン

プログラミングを中心にIT技術をできるだけわかりやすくまとめます。

  • ホーム
  • Ruby on Rails
  • 【Ruby on Rails】「bullet」、「N+1問題」について
 
 
     
  • サーバー言語  
    • Python
    • Ruby
    • PHP
    • SQL
  •  
  • インフラ  
       
    • AWS
    •  
    • 基本
    • Git
  • Web
       
    • Web開発
    • JavaScript
    • Vue.js
    • React
  •  
  • 設計  
       
    • 実装設計
    • DB設計
  • 問い合わせ
  

【Ruby on Rails】「bullet」、「N+1問題」について

01.09

  • miyabisan2
  • コメントを書く

この記事は3分で読めます

準備

まずは、モデルを作成します。

1
2
rails g model user name:string
rails g model post name:string

マイグレーションファイルをそれぞれ作成します。

user

1
2
3
4
5
6
7
8
9
class CreateUsers < ActiveRecord::Migration[6.0]
  def change
    create_table :users do |t|
      t.string :name
 
      t.timestamps
    end
  end
end

post

1
2
3
4
5
6
7
8
9
10
class CreatePosts < ActiveRecord::Migration[6.0]
  def change
    create_table :posts do |t|
      t.string :name
      t.references :user
 
      t.timestamps
    end
  end
end

modelファイルもそれぞれ設定します。

user

1
2
3
class User < ApplicationRecord
  has_many :posts
end

post

1
2
3
class Post < ApplicationRecord
  belongs_to :user
end

データ投入

「user:post = 1:多」になるようにデータを作成します。(rails cでRailsコンソール上で実行すると良いでしょう。)、ただ、userは1ですが、N+1問題を再現するために複数人作成します。

user(1)

1
2
3
user1 = User.create!(name:"太郎")
user2 = User.create!(name:"花子")
user3 = User.create!(name:"次郎")

post(多)

1
2
3
4
5
6
7
8
user1.posts.create!(name:"投稿1")
user1.posts.create!(name:"投稿2")
user2.posts.create!(name:"投稿1")
user2.posts.create!(name:"投稿2")
user2.posts.create!(name:"投稿3")
user3.posts.create!(name:"投稿1")
user3.posts.create!(name:"投稿2")
user3.posts.create!(name:"投稿3")

コントローラの作成

1
rails g controller users index

コントローラ側

1
2
3
4
5
class UsersController < ApplicationController
  def index
    @users = User.all
  end
end

ビュー側

1
2
3
4
5
6
7
<% @users.each do |user| %>
  <%= user.name %> |
  <% user.posts.each do |post| %>
    <%= post.name %>
  <% end %>
  <br>
<% end %>

url

1
http://localhost:3000/users

上記URLにアクセスすると下記のような感じでselect文が大量に発行されるN+1問題が発生します。

bulletの導入

bulletは開発環境でしか基本使わないのでdevelopmentグループ配下に設定します。

1
2
3
group :development do
  gem 'bullet'
end

bundleインストールします。

1
bundle install

以下の場所の設定ファイルにbulletで検知できるように設定します。

1
config/environments/development.rb

設定内容

1
2
3
4
5
6
7
config.after_initialize do
  Bullet.enable = true
  Bullet.alert = true
  Bullet.bullet_logger = true
  Bullet.console = true
  Bullet.rails_logger = true
end

Railsサーバーを再起動します。

1
rails s

また、上記URLにアクセスすると下記のポップアップ表示がなされています。

ポップアップの指示通りコントローラのクエリに「includes(:posts)」を追加します。

1
@users = User.all.includes(:posts)

再びページにアクセスすると今度はSQLが下記のようになりポップアップが出なくなっています。

1
2
3
  User Load (0.3ms)  SELECT "users".* FROM "users"
  ↳ app/views/users/index.html.erb:1
  Post Load (0.2ms)  SELECT "posts".* FROM "posts" WHERE "posts"."user_id" IN (?, ?, ?)  [["user_id", 1], ["user_id", 2], ["user_id", 3]]

余分な場合もある。

余分にincludes等のN+1対策をしてしまっている場合は逆に下記のようなメッセージが出ます。

1
2
AVOID eager loading detected
Remove to your query: .includes([:モデル名])

その場合はincludes等を外しましょう。

N+1問題とは?

Railsでループ処理の都度SQLを発行してしまいパフォーマンスが低下する問題のことです。

具体的にはどんな動作か?

1:多のリレーションのテーブル同士で1に紐づいている多の情報を取得しようとした際に、最初に一括で1の方のテーブルのデータをSQLで取得した後に、他の多の方の関連データを1件1件ずつ合計N件になるまでSQLを発行し続けます。数件程度であれば問題ないのですがデータ量が増えると多大なパフォーマンスの損失に繋がりかねません。

具体例

具体的にはjoinsを利用していて結合したテーブルをループして関連先のテーブルのデータを参照しようとした場合に発生します。

1
2
3
4
5
6
7
8
9
10
projects = Project.joins(:category_projects)
  Project Load (7.5ms)  SELECT "projects".* FROM "projects" INNER JOIN "category_projects" ON "category_projects"."project_id" = "projects"."id" LIMIT ?  [["LIMIT", 11]]
=> #<ActiveRecord::Relation [#<Project id: 1, title: "project1", created_at: "2020-12-19 09:54:11", updated_at: "2020-12-19 09:54:11">, #<Project id: 2, title: "project2", created_at: "2020-12-19 09:54:40", updated_at: "2020-12-19 09:54:40">]>
 
irb(main):011:0> projects.each do |project| p project.category_projects end
  CategoryProject Load (0.8ms)  SELECT "category_projects".* FROM "category_projects" WHERE "category_projects"."project_id" = ? LIMIT ?  [["project_id", 1], ["LIMIT", 11]]
#<ActiveRecord::Associations::CollectionProxy [#<CategoryProject id: 1, category_id: 1, project_id: 1, created_at: "2020-12-19 09:56:20", updated_at: "2020-12-19 09:56:20">]>
  CategoryProject Load (0.5ms)  SELECT "category_projects".* FROM "category_projects" WHERE "category_projects"."project_id" = ? LIMIT ?  [["project_id", 2], ["LIMIT", 11]]
#<ActiveRecord::Associations::CollectionProxy [#<CategoryProject id: 2, category_id: 1, project_id: 2, created_at: "2020-12-19 09:56:28", updated_at: "2020-12-19 09:56:28">]>
=> [#<Project id: 1, title: "project1", created_at: "2020-12-19 09:54:11", updated_at: "2020-12-19 09:54:11">, #<Project id: 2, title: "project2", created_at: "2020-12-19 09:54:40", updated_at: "2020-12-19 09:54:40">]

上記例では「Project:Category_project」=「1:多」になっている場合で関連先のテーブルを呼ぶたびにSELECT文が呼ばれていることがわかります。これはjoinsの特性としてメモリ上に関連先テーブル情報(CategoryProject)までキャッシュしないためです。

N+1問題への対策

下記のActiveRecordのメソッドを使うことにより比較的パフォーマンスを負荷を軽減できます。どちらも処理の前にassociationをキャッシュすることによりN回のSQL実行を1回にまとめることができます。

  • preloadメソッド
  • eager_loadメソッド

preloadメソッド

IN句を使ってSQLを一つにまとめます。あまりJOINしたくないでかいテーブル同士の結合の際はこちらを使うと良いでしょう。

1
2
3
4
5
6
7
8
9
10
11
irb(main):019:0> projects = Project.preload(:category_projects)
  Project Load (5.7ms)  SELECT "projects".* FROM "projects" LIMIT ?  [["LIMIT", 11]]
  CategoryProject Load (0.6ms)  SELECT "category_projects".* FROM "category_projects" WHERE "category_projects"."project_id" IN (?, ?)  [["project_id", 1], ["project_id", 2]]
=> #<ActiveRecord::Relation [#<Project id: 1, title: "project1", created_at: "2020-12-19 09:54:11", updated_at: "2020-12-19 09:54:11">, #<Project id: 2, title: "project2", created_at: "2020-12-19 09:54:40", updated_at: "2020-12-19 09:54:40">]>
 
irb(main):020:0> projects.each do |project| p project.category_projects end
  Project Load (0.8ms)  SELECT "projects".* FROM "projects"
  CategoryProject Load (0.4ms)  SELECT "category_projects".* FROM "category_projects" WHERE "category_projects"."project_id" IN (?, ?)  [["project_id", 1], ["project_id", 2]]
#<ActiveRecord::Associations::CollectionProxy [#<CategoryProject id: 1, category_id: 1, project_id: 1, created_at: "2020-12-19 09:56:20", updated_at: "2020-12-19 09:56:20">]>
#<ActiveRecord::Associations::CollectionProxy [#<CategoryProject id: 2, category_id: 1, project_id: 2, created_at: "2020-12-19 09:56:28", updated_at: "2020-12-19 09:56:28">]>
=> [#<Project id: 1, title: "project1", created_at: "2020-12-19 09:54:11", updated_at: "2020-12-19 09:54:11">, #<Project id: 2, title: "project2", created_at: "2020-12-19 09:54:40", updated_at: "2020-12-19 09:54:40">]

eager_loadの使い分けとしては、「1:多」でデータ量が多いテーブルの結合の際に使用すると良いでしょう。

whereが使えない。

相手先のテーブルの絞り込みをしようとして、whereを使うとエラーになります。

1
2
3
4
irb(main):030:0> projects = Project.preload(:category_projects).where(category_projects: { project_id:1} )
  Project Load (3.6ms)  SELECT "projects".* FROM "projects" WHERE "category_projects"."project_id" = ? LIMIT ?  [["project_id", 1], ["LIMIT", 11]]
Traceback (most recent call last):
ActiveRecord::StatementInvalid (SQLite3::SQLException: no such column: category_projects.project_id)

includesメソッド

IN句を使ってSQLを一つにまとめます。状況に応じて、preloadかeager_loadが使い分けられます。それほど実務で影響があるわけではありませんが、理解を深められた方であれば意図的にpreloadかeager_loadかを選択するようにしましょう。
1
2
3
4
5
6
7
8
9
10
11
irb(main):023:0> projects = Project.includes(:category_projects)
  Project Load (1.6ms)  SELECT "projects".* FROM "projects" LIMIT ?  [["LIMIT", 11]]
  CategoryProject Load (2.4ms)  SELECT "category_projects".* FROM "category_projects" WHERE "category_projects"."project_id" IN (?, ?)  [["project_id", 1], ["project_id", 2]]
=> #<ActiveRecord::Relation [#<Project id: 1, title: "project1", created_at: "2020-12-19 09:54:11", updated_at: "2020-12-19 09:54:11">, #<Project id: 2, title: "project2", created_at: "2020-12-19 09:54:40", updated_at: "2020-12-19 09:54:40">]>
 
irb(main):024:0> projects.each do |project| p project.category_projects end
  Project Load (0.4ms)  SELECT "projects".* FROM "projects"
  CategoryProject Load (4.4ms)  SELECT "category_projects".* FROM "category_projects" WHERE "category_projects"."project_id" IN (?, ?)  [["project_id", 1], ["project_id", 2]]
#<ActiveRecord::Associations::CollectionProxy [#<CategoryProject id: 1, category_id: 1, project_id: 1, created_at: "2020-12-19 09:56:20", updated_at: "2020-12-19 09:56:20">]>
#<ActiveRecord::Associations::CollectionProxy [#<CategoryProject id: 2, category_id: 1, project_id: 2, created_at: "2020-12-19 09:56:28", updated_at: "2020-12-19 09:56:28">]>
=> [#<Project id: 1, title: "project1", created_at: "2020-12-19 09:54:11", updated_at: "2020-12-19 09:54:11">, #<Project id: 2, title: "project2", created_at: "2020-12-19 09:54:40", updated_at: "2020-12-19 09:54:40">]

eager_loadメソッド

LEFT OUTER JOINを使ってSQLを一つにまとめます。

1
2
3
4
5
6
7
8
9
10
irb(main):015:0> projects = Project.eager_load(:category_projects)
  SQL (0.5ms)  SELECT DISTINCT "projects"."id" FROM "projects" LEFT OUTER JOIN "category_projects" ON "category_projects"."project_id" = "projects"."id" LIMIT ?  [["LIMIT", 11]]
  SQL (0.7ms)  SELECT "projects"."id" AS t0_r0, "projects"."title" AS t0_r1, "projects"."created_at" AS t0_r2, "projects"."updated_at" AS t0_r3, "category_projects"."id" AS t1_r0, "category_projects"."category_id" AS t1_r1, "category_projects"."project_id" AS t1_r2, "category_projects"."created_at" AS t1_r3, "category_projects"."updated_at" AS t1_r4 FROM "projects" LEFT OUTER JOIN "category_projects" ON "category_projects"."project_id" = "projects"."id" WHERE "projects"."id" IN (?, ?)  [["id", 1], ["id", 2]]
↑関連先が「多」になっていると上のSQLでDISTINCTが発生してしまいコストが増える。
 
irb(main):016:0> projects.each do |project| p project.category_projects end
  SQL (0.8ms)  SELECT "projects"."id" AS t0_r0, "projects"."title" AS t0_r1, "projects"."created_at" AS t0_r2, "projects"."updated_at" AS t0_r3, "category_projects"."id" AS t1_r0, "category_projects"."category_id" AS t1_r1, "category_projects"."project_id" AS t1_r2, "category_projects"."created_at" AS t1_r3, "category_projects"."updated_at" AS t1_r4 FROM "projects" LEFT OUTER JOIN "category_projects" ON "category_projects"."project_id" = "projects"."id"
#<ActiveRecord::Associations::CollectionProxy [#<CategoryProject id: 1, category_id: 1, project_id: 1, created_at: "2020-12-19 09:56:20", updated_at: "2020-12-19 09:56:20">]>
#<ActiveRecord::Associations::CollectionProxy [#<CategoryProject id: 2, category_id: 1, project_id: 2, created_at: "2020-12-19 09:56:28", updated_at: "2020-12-19 09:56:28">]>
=> [#<Project id: 1, title: "project1", created_at: "2020-12-19 09:54:11", updated_at: "2020-12-19 09:54:11">, #<Project id: 2, title: "project2", created_at: "2020-12-19 09:54:40", updated_at: "2020-12-19 09:54:40">]

joinsと同じように記述していますが、ループを回す際のSQLの実行が1回で済んでいるのでSQL発行回数は最小に抑えることができます。ただ、相手が関連先テーブルがNの場合は最初に結合する際に、DISTINCTが発生するのでデータ量が少なく、「1:1」の関連の場合のみ使用すると良いでしょう。

whereも使える。

eager_loadなら相手先のテーブルで絞り込んだ結果を返せます。

1
2
3
4
irb(main):029:0> projects = Project.eager_load(:category_projects).where(category_projects: { project_id:1} )
  SQL (1.7ms)  SELECT DISTINCT "projects"."id" FROM "projects" LEFT OUTER JOIN "category_projects" ON "category_projects"."project_id" = "projects"."id" WHERE "category_projects"."project_id" = ? LIMIT ?  [["project_id", 1], ["LIMIT", 11]]
  SQL (174.9ms)  SELECT "projects"."id" AS t0_r0, "projects"."title" AS t0_r1, "projects"."created_at" AS t0_r2, "projects"."updated_at" AS t0_r3, "category_projects"."id" AS t1_r0, "category_projects"."category_id" AS t1_r1, "category_projects"."project_id" AS t1_r2, "category_projects"."created_at" AS t1_r3, "category_projects"."updated_at" AS t1_r4 FROM "projects" LEFT OUTER JOIN "category_projects" ON "category_projects"."project_id" = "projects"."id" WHERE "category_projects"."project_id" = ? AND "projects"."id" = ?  [["project_id", 1], ["id", 1]]
=> #<ActiveRecord::Relation [#<Project id: 1, title: "project1", created_at: "2020-12-19 09:54:11", updated_at: "2020-12-19 09:54:11">]>

includesメソッド

例えば、Userテーブルに1:多のリレーションがあるpcテーブルがある場合に下記のように記述する場合

1
User.pc

上記のように記述するとUserごとにslect文を発行して1つずつpcを取得する動作になってしまいます。

1
User.includes(:pc)

includesを使えばIN句を使ってユーザーに紐づくpcを一括して取得することができます。

スポンサーリンク
  • 2021 01.09
  • miyabisan2
  • コメントを書く
  • Ruby on Rails
  • Tweets Twitter
  • このエントリーをはてなブックマークに追加
  • LINEで送る

関連記事

  1. 2020 10.13

    【Ruby on Rails】「pundit」について

  2. 2019 12.04

    【Ruby on Rails】アセットの設定ファイル(assets.rb、config/environments)

  3. 2018 04.01

    【Webセキュリティ】XSS(クロスサイトスクリプティング)対策の基本(JavaやRails、breakmanなど)

  4. 2019 11.30

    【Ruby on Rails】パーシャルを使ったビューの共通化、ロジックの共通化、HtmlBuilder

  5. 2019 11.27

    【Ruby on Rails】RailsにBootstrapを導入する。

  6. 2020 12.12

    【Ruby on Rails】「プレゼンター(デコレーター)」について

  • コメント ( 0 )
  • トラックバック ( 0 )
  1. この記事へのコメントはありません。

  1. この記事へのトラックバックはありません。

返信をキャンセルする。

【AWS】DynamoDBの基本的な仕組み

【AWS】「SNS」について

RETURN TOP

著者プロフィール

エンジニア歴10年で過去に業務系、Webデザイン、インフラ系なども経験あります。現在はWeb系でフロントエンド開発中心です。

詳細なプロフィールはこちら

スポンサーリンク

カテゴリー

  • Android
  • AngularJS
  • API
  • AWS
  • C++
  • CSS
  • C言語
  • DDD
  • DevOps
  • Django
  • Docker
  • Figma
  • Git
  • GitLab
  • GraphQL
  • Hasura
  • Java
  • JavaScript
  • Kubernetes
  • Laravel
  • linux
  • MySQL
  • Next.js
  • nginx
  • Node.js
  • NoSQL
  • Nuxt.js
  • Oracle
  • PHP
  • Python
  • React
  • Redux
  • Rspec
  • Ruby
  • Ruby on Rails
  • Sass
  • Spring Framework
  • SQL
  • TypeScript
  • Unity
  • Vue.js
  • Webサービス開発
  • Webデザイン
  • Web技術
  • インフラ
  • オブジェクト指向
  • システム開発
  • セキュリティ
  • その他
  • データベース
  • デザインパターン
  • テスト
  • ネットワーク
  • プログラミング全般
  • マイクロサービス
  • マイクロソフト系技術
  • マルチメディア
  • リファクタリング
  • 副業
  • 未分類
  • 業務知識
  • 生成AI
  • 設計
  • 関数型言語
RETURN TOP

Copyright ©  プログラミングマガジン | プライバシーポリシー