k0kubun's blog

railsへの執着はもはや煩悩の域であり、開発者一同は瞑想したほうがいいと思います。

#shibuyarb でRailsのN+1 countクエリについてLTしました

追記: 2017/09/06

少しAPIが冗長なもののActiveRecordへのモンキーパッチが少ないバージョン activerecord-precounter というのを作りました。こちらの方がバグりにくいはずなので、現在はactiverecord-precounterの方を使うことが推奨されます。

github.com

#shibuyarb でLTをした

ぼくが一番長い期間書いている言語はRubyなんだけど、なぜかGoの話ばかりしていてRubyについての発表をしたことがなかった。
新卒研修も終わって平日やっている勉強会にも気軽に出れる感じになったので、以前作ったactiverecord-precountというgemについて発表をしてきた。

ActiveRecordのN+1 countクエリどうすんの問題

Tweet.all.each do |tweet|
  tweet.favorites.count
end
# SELECT `tweets`.* FROM `tweets`
# SELECT COUNT(*) FROM `favorites` WHERE `favorites`.`tweet_id` = 1
# SELECT COUNT(*) FROM `favorites` WHERE `favorites`.`tweet_id` = 2
# SELECT COUNT(*) FROM `favorites` WHERE `favorites`.`tweet_id` = 3
# SELECT COUNT(*) FROM `favorites` WHERE `favorites`.`tweet_id` = 4
# SELECT COUNT(*) FROM `favorites` WHERE `favorites`.`tweet_id` = 5

みたいなやつがあるとき、

  • 基本的にはRails標準のcounter_cacheを使う
  • あんまりALTER TABLEしたくないとか、カラムを増やすほどでもないときはeager loading
    • Railsにその手段が用意されていないので、その機能を入れてくれるのがactiverecord-precount gem

という話をした。

使い方

使い方については RailsでN+1 countクエリを潰すactiverecord-precountを作った にも書いたけど、

Tweet.all.precount(:favorites).each do |tweet|
  p tweet.favorites.count
end
# SELECT `tweets`.* FROM `tweets`
# SELECT COUNT(`favorites`.`tweet_id`), `favorites`.`tweet_id` FROM `favorites` WHERE `favorites`.`tweet_id` IN (1, 2, 3, 4, 5) GROUP BY `favorites`.`tweet_id`
Tweet.all.eager_count(:favorites).each do |tweet|
  p tweet.favorites.count
end
# SELECT `tweets`.`id` AS t0_r0, `tweets`.`tweet_id` AS t0_r1, `tweets`.`user_id` AS t0_r2, `tweets`.`created_at` AS t0_r3, `tweets`.`updated_at` AS t0_r4, COUNT(`favorites`.`id`) AS t1_r0 FROM `tweets` LEFT OUTER JOIN `favorites` ON `favorites`.`tweet_id` = `tweets`.`id` GROUP BY tweets.id

みたいにするだけなので、別にカラムとか追加しないから、いざ何か問題が起きてもロールバックしやすいと思う。内部的にはActiveRecord::Baseのインスタンスを余計に作ったりしないから速い。
(単にCOUNTしたIntegerをいれておくだけ。)

放置されているN+1 countクエリを見かけたら是非お試し下さい。