課題
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モデルは一対一の関係で成り立っています。
class Group < ApplicationRecord
has_many :users
end
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」を使うことによって開発を効率化することができます。
group :development do
gem 'bullet'
end
Gemfileの開発環境のグループにbelletを記述しbundle install
します。
次にbundle exec rails g bullet:install
をターミナルで走らせると自動で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問題が発生した時に自動でアラートで知らせてくれるようになります。