RailsでN+1 countクエリを潰すactiverecord-precountを作った
追記: 2017/09/06
少しAPIが冗長なもののActiveRecordへのモンキーパッチが少ないバージョン activerecord-precounter というのを作りました。こちらの方がバグりにくいはずなので、現在はactiverecord-precounterの方を使うことが推奨されます。
概要
N+1 countクエリを最大11.7倍速くできるactiverecord-precountというgemを作った。 *1
k0kubun/activerecord-precount
N+1 countクエリ
Tweet.all.each do |tweet| p 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
私がN+1 countクエリと呼んでいるのはこういう奴で、見たことがある人は多いと思う。 これを解決するには、基本的にはcounter_cacheを使うしかない。 *2
countクエリのeager loading
しかし、スキーマを冗長化したら速くなるのは当たり前で、そんな手間をかけなくてもクエリをチューニングすればそこそこ速くなる。 で、それをActiveRecordで簡単に実現する方法がないのが問題で、そのAPIを提供するためにactiverecord-precount gemを作った。
このgemには、eager loadingのためのAPIが2つある。
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`
普通にデータを取るクエリとeager loadingのためのクエリを分けて実行するのがprecount。 クエリを分割してeager loadingを行うpreloadにちなんで命名している。
このベンチ
では、precount(:favorites)
を呼ぶことで最大7.9倍速くなる。
eager_count
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
JOINを使ってクエリ1つでeager loadingも行うのがeager_count。 似た挙動をするeager_loadにちなんで命名した。 なんとなくJOINしたくない気分のときにprecountの方を使うことを想定している(雑)。
このベンチ
では、eager_count(:favorites)
を呼ぶことで最大11.7倍速くなっている。
counter_cacheは同じ条件で20.0倍なので、手間をかけずに11.7倍か、コストかけて20倍にするかで実装を選択すると良さそう。
使い方
Gemfileに以下を追加してbundle installするとrelation上でprecountやeager_countが使えるようになります。
gem 'activerecord-precount'
N+1 countクエリを見かけることがあったらお試しください。