LoginSignup
1
0

More than 1 year has passed since last update.

Rails N+1問題への対処

Posted at

課題

RailsのActiveRecordはRubyでデータベースとの問い合わせができる便利なツールである一方、発行されるSQLを意識しなくても済んでしまうため、効率の悪いクエリを発行していても気づかない危険性があります。Railsにおいては特にN+1問題という必要以上にデータベースに問い合わせをしてしまい、パフォーマンスを低下させてしまう問題がよく知られています。

N+1問題とは?

N+1問題とは、「ループ処理の中で都度SQLを発行してしまい、大量のSQLが実行されパフォーマンスが低下する」という問題のことです。100件あれば100回、1000件あれば1000回、ループ処理の中で問い合わせすることになるので、いかにも非効率的です。馬鹿馬鹿しい初歩的なミスに聞こえますが、Railsを使用していると往々にしてこの問題が生じます。

では具体的なN+1問題の例を見ていきましょう。
まず、usersテーブルとpostsテーブルがあると仮定しましょう。

groupsテーブル

id name
1 A
2 B
3 C

usersテーブル

id group_id name
1 1 一郎
2 2 二郎
3 3 三郎
4 2 四郎

UserモデルとGroupモデルは一対一の関係で成り立っています。

app/models/group.rb
class Group < ApplicationRecord
  has_many :users
end
app/models/user.rb
class User < ApplicationRecord
  belongs_to :group
end

では全てのグループに所属する人を表示するプログラムを書いてみます。

Group.all.each do |group|
  user_names = comp.users.pluck(:name).join(",")
  p "#{group.name}: #{user_names}"
end

上記のコードでは、まずGroup.allでgroupsテーブルから全件レコードをとってきて、その結果に対しそれぞれ紐づけられたusersの情報を表示しています。結果は以下のようになります。

  Group Load (0.3ms)  SELECT `groups`.* FROM `groups`
   (0.3ms)  SELECT `users`.`name` FROM `users` WHERE `users`.`group_id` = 1
"A: 一郎"
   (0.3ms)  SELECT `users`.`name` FROM `users` WHERE `users`.`group_id` = 2
"B: 二郎、四郎"
   (0.3ms)  SELECT `users`.`name` FROM `users` WHERE `users`.`group_id` = 3
"C: 三郎"

まず一回目のSQLでgroupsテーブルの全ての情報を問い合わせ、その後各グループごとに所属ユーザーの問い合わせをしていることがわかります。n回ループをした場合、N+1回の問い合わせが生じてしまうため、N+1問題と言われています。
SQLを見れば非効率的なDB問い合わせをしているのがわかるのですが、このとおりRailsではSQLを意識することなくコードがかけてしまうため、知らず知らずのうちにこういった問題が起こります。今回の例ではデータが少ないためそこまでパフォーマンスに問題が出るわけでないですが、データ量が大きくなってくると無視できないレベルまでパフォーマンスが悪化してしまいます。特にRailsを使うようなWebアプリケーションではミリ単位のパフォーマンスが重視されるため、気をつけて実装する必要があります。

解決方法

先ほどのuserとgroupの例で言うと、Group.allでgroupの情報をとってくる段階でuserの情報を取って来れれば問題は解決しそうですね。そのためにはincludes()と言うメソッドを使います。

Group.all.includes(:users).each do |group|
  user_names = comp.users.pluck(:name).join(",")
  p "#{group.name}: #{user_names}"
end
SELECT `group`.* FROM `groups`
SELECT 'users'.* FROM 'users' WHERE 'users'.'user_id' IN (1,2,3,4,5)

今度はSQLが二つだけで済みました。

N+1問題を検知するgem「bullet」

今回のように単純な構造のRDSなら自分でもログを見ていれば気がつけますが、複雑なシステム伴ってくると見落とす可能性もありますし、解決するのに時間がかかる可能性があります。
そこでN+1問題を自動で検知して解決方法を自動で提案してくれるgem「bullet」を使うことによって開発を効率化することができます。

Gemfile
group :development do
  gem 'bullet'
end

Gemfileの開発環境のグループにbelletを記述しbundle installします。
次にbundle exec rails g bullet:installをターミナルで走らせると自動でconfig/environments/development.rbに詳細な設定が追記されます。

config/environments/development.rb
config.after_initialize do
  Bullet.enable        = true
  Bullet.alert         = true
  Bullet.bullet_logger = true
  Bullet.console       = true
# Bullet.growl         = true 
  Bullet.rails_logger  = true
  Bullet.add_footer    = true
end

それぞれの項目については公式のdocumentを読んで適宜調整してください。
これで準備完了です。これでアプリを走らせれば、N+1問題が発生した時に自動でアラートで知らせてくれるようになります。

1
0
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
1
0