【Rails】gem使ってみたシリーズ 〜bullet編〜 N+1問題を可視化
はじめに
お疲れ様です。
おおくまです。
今回は、「【Rails】gem使ってみたシリーズ 〜bullet編〜 N+1問題を可視化」ということで、Ruby on Railsで、bulletというgemを使って、N+1問題を可視化する方法について、まとめてみました。
少しでも皆様の参考になりますと幸いです。
対象読者
注意点
環境
N+1問題とは
N+1問題とは、データベースへの不要なクエリが大量に発生することで、パフォーマンスが低下する問題のことです。
そのため、データベースに高負荷がかかってしまったり、レスポンスが遅くなってしまったりすることがあります。
Ruby on Railsにおいては、ActiveRecordの関連(belongs_to
やhas_many
)を扱うときに発生しやすいです。
発生例
以下のようなUser
テーブルとPost
テーブルがあるとします。
ActiveRecord::Schema[7.0].define(version: 2024_12_21_083443) do
create_table "posts", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.bigint "user_id", null: false
t.string "title", null: false
t.text "content", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["user_id"], name: "index_posts_on_user_id"
end
create_table "users", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.string "name", null: false
t.string "email", null: false
t.integer "age", null: false
t.string "login_id", null: false
t.string "encrypted_password", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["email"], name: "index_users_on_email", unique: true
t.index ["login_id"], name: "index_users_on_login_id", unique: true
end
add_foreign_key "posts", "users"
end
class User < ApplicationRecord
has_many :posts, dependent: :destroy
end
class Post < ApplicationRecord
belongs_to :user
end
ここで、Post
一覧画面を作成し、わざとN+1問題を発生させてみます。
class PostsController < ApplicationController
def index
@posts = Post.all
end
end
div.p-5
h1.mb-3 投稿一覧
p style="color: green" = notice
= link_to "新規追加", new_post_path, class: 'btn btn-outline-info btn-sm m-3'
.d-flex.flex-wrap.align-items-center
- @posts.each do |post|
.card.border.m-3 style="width: 400px;"
.card-header.h5 = post.title
.card-body
.float-right
button.copy-button.btn.btn-outline-secondary.btn-sm data-target="post_#{post.id}"
i.far.fa-copy.mr-1
span.copy-text コピー
div
.mb-3 = post.user.name
div id="post_#{post.id}"
.mb-3 = post.content
= link_to "詳細", post, class: 'btn btn-outline-secondary btn-sm mr-1'
- if current_user.id == post.user_id
= link_to "編集", edit_post_path(post), class: 'btn btn btn-outline-info btn-sm mr-1'
= link_to "削除", post, method: :delete, data: { turbo: "true", turbo_method: :delete, turbo_confirm: '削除しますか?' }, class: "btn btn-outline-danger btn-sm"
このように、コントローラーではPost
の情報のみを取得し、ビューを描画する際に、個々にUser
の情報を取得しているため、N+1問題が発生しています。
あわせて、発行されたクエリも確認してみます。
Processing by PostsController#index as HTML
[1m[36mUser Load (2.5ms)[0m [1m[34mSELECT `users`.* FROM `users` WHERE `users`.`id` = 1 ORDER BY `users`.`id` ASC LIMIT 1 /*application:TestApp,controller:posts,action:index*/[0m
↳ app/controllers/application_controller.rb:9:in `authenticate_user!'
Rendering layout layouts/application.html.slim
Rendering posts/index.html.slim within layouts/application
[1m[36mPost Load (0.6ms)[0m [1m[34mSELECT `posts`.* FROM `posts` ORDER BY `posts`.`id` ASC /*application:TestApp,controller:posts,action:index*/[0m
↳ app/views/posts/index.html.slim:6
[1m[36mUser Load (0.1ms)[0m [1m[34mSELECT `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1 /*application:TestApp,controller:posts,action:index*/[0m
↳ app/views/posts/index.html.slim:15
[1m[36mUser Load (0.1ms)[0m [1m[34mSELECT `users`.* FROM `users` WHERE `users`.`id` = 2 LIMIT 1 /*application:TestApp,controller:posts,action:index*/[0m
↳ app/views/posts/index.html.slim:15
[1m[36mUser Load (0.1ms)[0m [1m[34mSELECT `users`.* FROM `users` WHERE `users`.`id` = 3 LIMIT 1 /*application:TestApp,controller:posts,action:index*/[0m
↳ app/views/posts/index.html.slim:15
[1m[36mUser Load (0.1ms)[0m [1m[34mSELECT `users`.* FROM `users` WHERE `users`.`id` = 4 LIMIT 1 /*application:TestApp,controller:posts,action:index*/[0m
↳ app/views/posts/index.html.slim:15
[1m[36mUser Load (0.1ms)[0m [1m[34mSELECT `users`.* FROM `users` WHERE `users`.`id` = 5 LIMIT 1 /*application:TestApp,controller:posts,action:index*/[0m
↳ app/views/posts/index.html.slim:15
[1m[36mUser Load (0.1ms)[0m [1m[34mSELECT `users`.* FROM `users` WHERE `users`.`id` = 6 LIMIT 1 /*application:TestApp,controller:posts,action:index*/[0m
↳ app/views/posts/index.html.slim:15
[1m[36mUser Load (0.1ms)[0m [1m[34mSELECT `users`.* FROM `users` WHERE `users`.`id` = 7 LIMIT 1 /*application:TestApp,controller:posts,action:index*/[0m
↳ app/views/posts/index.html.slim:15
[1m[36mUser Load (0.1ms)[0m [1m[34mSELECT `users`.* FROM `users` WHERE `users`.`id` = 8 LIMIT 1 /*application:TestApp,controller:posts,action:index*/[0m
↳ app/views/posts/index.html.slim:15
[1m[36mUser Load (0.1ms)[0m [1m[34mSELECT `users`.* FROM `users` WHERE `users`.`id` = 9 LIMIT 1 /*application:TestApp,controller:posts,action:index*/[0m
↳ app/views/posts/index.html.slim:15
Rendered posts/index.html.slim within layouts/application (Duration: 13.3ms | Allocations: 7565)
Rendered layout layouts/application.html.slim (Duration: 29.2ms | Allocations: 11491)
Completed 200 OK in 61ms (Views: 28.9ms | ActiveRecord: 5.7ms | Allocations: 12857)
ビューを描画する際に、User
テーブルに対して、Post
の数だけクエリが発行されていることがわかります。
Post
の数が増えるほど、発行されるクエリの数も増えてしまうため、パフォーマンスが低下してしまいます。
解決方法
今回の例では、コントローラーでPost
の情報を取得する際に、User
の情報も一緒に取得するように修正することで、N+1問題を解消することができます。
class PostsController < ApplicationController
def index
+ @posts = Post.includes(:user)
end
end
Processing by PostsController#index as HTML
[1m[36mUser Load (1.6ms)[0m [1m[34mSELECT `users`.* FROM `users` WHERE `users`.`id` = 1 ORDER BY `users`.`id` ASC LIMIT 1 /*application:TestApp,controller:posts,action:index*/[0m
↳ app/controllers/application_controller.rb:9:in `authenticate_user!'
Rendering layout layouts/application.html.slim
Rendering posts/index.html.slim within layouts/application
[1m[36mPost Load (0.4ms)[0m [1m[34mSELECT `posts`.* FROM `posts` ORDER BY `posts`.`id` ASC /*application:TestApp,controller:posts,action:index*/[0m
↳ app/views/posts/index.html.slim:6
[1m[36mUser Load (0.3ms)[0m [1m[34mSELECT `users`.* FROM `users` WHERE `users`.`id` IN (1, 2, 3, 4, 5, 6, 7, 8, 9) /*application:TestApp,controller:posts,action:index*/[0m
↳ app/views/posts/index.html.slim:6
Rendered posts/index.html.slim within layouts/application (Duration: 18.3ms | Allocations: 7235)
Rendered layout layouts/application.html.slim (Duration: 57.7ms | Allocations: 37078)
Completed 200 OK in 79ms (Views: 57.9ms | ActiveRecord: 3.4ms | Allocations: 41825)
発行されたクエリの数が減ったことが分かります。
コードを少し修正するだけで、N+1問題を解消することができました。
しかし、普段、コードを書いている中で、N+1問題が発生しているかどうかを把握することは難しいです。
そこで、bulletというgemを使うことで、N+1問題を可視化することができます。
bulletとは
bulletは、N+1問題を可視化するためのgemです。
bulletを導入することで、N+1問題が発生した際に教えてくれるため、N+1問題を解消することができます。
導入方法
まず、Gemfileにbulletを追加し、bundle install
を実行します。
次に、bundle exec rails g bullet:install
を実行します。
そうすると、config/environments/development.rb
に以下のような設定が追加されます。
Rails.application.configure do
config.after_initialize do
Bullet.enable = true
Bullet.alert = true
Bullet.bullet_logger = true
Bullet.console = true
Bullet.rails_logger = true
Bullet.add_footer = true
end
end
他にも様々な設定がありますが、今回はデフォルトの設定で進めていきます。
これで、bulletの導入が完了しました。
N+1問題を可視化
先ほど修正したPost
一覧画面で、N+1問題を再度、発生させてみます。
class PostsController < ApplicationController
def index
+ @posts = Post.all
end
end
Post
一覧画面を表示すると、N+1問題が発生していることが可視化されます。
アラート
Post
一覧画面を表示すると、アラートが表示されました。
log/bullet.log
2025-02-11 17:24:22[WARN] user: root
GET /
USE eager loading detected
Post => [:user]
Add to your query: .includes([:user])
Call stack
/test_app/app/views/posts/index.html.slim:15:in `block in _app_views_posts_index_html_slim___3451928325704220638_26000'
/test_app/app/views/posts/index.html.slim:6:in `_app_views_posts_index_html_slim___3451928325704220638_26000'
log/bullet.log
にも、ログが出力されました。
開発者ツールのコンソール
開発者ツールのコンソールにも、アラートが表示されました。
log/development.log
user: root
GET /
USE eager loading detected
Post => [:user]
Add to your query: .includes([:user])
Call stack
/test_app/app/views/posts/index.html.slim:15:in `block in _app_views_posts_index_html_slim___3451928325704220638_26000'
/test_app/app/views/posts/index.html.slim:6:in `_app_views_posts_index_html_slim___3451928325704220638_26000'
log/development.log
にも、ログが出力されました。
フッター
フッターにも、アラートが表示されました。
まとめ
今回、N+1問題や、N+1問題を可視化するためのgemであるbulletについて、まとめてみました。
導入も簡単で、N+1問題を可視化することができるため、開発中にN+1問題が発生してしまっても、すぐに気づくことができます。
こちらを参考に、アプリケーションのパフォーマンスを向上させてみてください。
少しでも皆様の参考になりますと幸いです。
最後まで読んでいただき、ありがとうございました。
Discussion