N+1問題を発見しDBのクエリを改善するBullet


Ruby on RailsのActive Recordのクエリ改善を行うためのGem「Bullet」を紹介します。

Bulletは開発環境でN+1のクエリを見つけたら警告を出してくれるgemです。
N+1問題とはデータ量(N+1)回のクエリを実行してしまい、レスポンスが悪くなる問題です。

Bulletによるアラート

🎃 インストール手順

Gemfileに以下を追加して、bundle installを実行します。

group :development do
# N+1問題のクエリを警告
gem 'bullet'
end

config/environments/development.rbに次の設定を追加します。

AppName::Application.configure do
config.after_initialize do
Bullet.enable = true # Bulletプラグインを有効
Bullet.alert = true # JavaScriptでの通知
Bullet.bullet_logger = true # log/bullet.logへの出力
Bullet.console = true # ブラウザのコンソールログに記録
Bullet.rails_logger = true # Railsログに出力
end
end

🚜 N+1問題の解消方法

N+1の警告が発生するようなクエリを見つけたらincludeでSQLの実行時に合わせて関連テーブルを読み込みます。

def index
@articles = Articles.include(:user).all
end

修正前はArticlesのレコード分読み込んでいました。

Started GET "/articles" for 127.0.0.1 at 2017-05-12 19:59:04 +0900
Processing by ArticlesController#index as HTML
Rendering articles/index.html.haml within layouts/application
Article Load (0.3ms) SELECT "articles".* FROM "articles"
User Load (0.6ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
User Load (0.4ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2 [["id", 2], ["LIMIT", 1]]
User Load (0.3ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2 [["id", 3], ["LIMIT", 1]]
User Load (0.3ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2 [["id", 4], ["LIMIT", 1]]
User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2 [["id", 5], ["LIMIT", 1]]
User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2 [["id", 6], ["LIMIT", 1]]
User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2 [["id", 7], ["LIMIT", 1]]
User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2 [["id", 8], ["LIMIT", 1]]
User Load (0.3ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2 [["id", 9], ["LIMIT", 1]]
User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2 [["id", 10], ["LIMIT", 1]]

修正後は2つのSQLクエリで完了していることがわかります。

Started GET "/articles" for 127.0.0.1 at 2017-05-12 20:03:07 +0900
Processing by ArticlesController#index as HTML
Rendering articles/index.html.haml within layouts/application
Article Load (5.5ms) SELECT "articles".* FROM "articles"
User Load (12.4ms) SELECT "users".* FROM "users" WHERE "users"."id" IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

🎉 Bulletのホワイトリスト設定

Bulletを使っている中でN+1の警告を無視したい場合はホワイトリスト形式でconfig/initializer/bullet.rbに登録しておきましょう。

# N+1クエリが発生しても警告を出さない
Bullet.add_whitelist type: :n_plus_one_query, class_name: 'Post', association: :comments
# 使っていないeager_loadingを許容する
Bullet.add_whitelist type: :unused_eager_loading, class_name: 'Post', association: :comments
# 不必要なcountの検出を許容する
Bullet.add_whitelist type: :counter_cache, class_name: "Country", association: :cities

🍣 補足:Ruby/Railsのバージョン情報

本件の動作検証の環境は次のとおりです。

  • Ruby 2.4.1
  • Rails 5.1.0
  • Bullet 5.5.1

🐮 参考リンク

🖥 VULTRおすすめ

VULTR」はVPSサーバのサービスです。日本にリージョンがあり、最安は512MBで2.5ドル/月($0.004/時間)で借りることができます。4GBメモリでも月20ドルです。 最近はVULTRのヘビーユーザーになので、「ここ」から会員登録してもらえるとサービス開発が捗ります!

📚 おすすめの書籍