プログラミングマガジン

プログラミングを中心に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 11.22

    【Ruby on Rails】「画面パラメータの渡し方」の種類

  2. 2020 01.10

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

  3. 2022 09.04

    【Rails】「ロードパス」、「Zeitwerk」について

  4. 2019 12.14

    【VSCode】Railsのデバッグ設定(Mac)

  5. 2019 12.01

    【Rspec】RspecのインストールやRailsで使うための準備や実行方法

  6. 2021 01.30

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

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

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

返信をキャンセルする。

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

【AWS】「SNS」について

RETURN TOP

著者プロフィール

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

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

スポンサーリンク

カテゴリー

  • Android
  • AngularJS
  • API
  • AWS
  • C++
  • CSS
  • cursor
  • C言語
  • DDD
  • DevOps
  • Django
  • Docker
  • Figma
  • Git
  • GitLab
  • GraphQL
  • gRPC
  • 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 ©  プログラミングマガジン | プライバシーポリシー