N+1カウントクエリとは、取得したN件のデータそれぞれに対してcountクエリが発行される現象のことを指します。
以下はN+1カウントクエリの例です。取得したN件のauthorに対してcountクエリが実行されていることがわかります。
コード
authors = Author.all
authors.each do |author|
author.books.count
end
実行ログ
Author Load (0.5ms) SELECT `authors`.* FROM `authors` ORDER BY `authors`.`id` ASC LIMIT 1000
(0.6ms) SELECT COUNT(*) FROM `books` WHERE `books`.`author_id` = 1
(0.6ms) SELECT COUNT(*) FROM `books` WHERE `books`.`author_id` = 2
(0.5ms) SELECT COUNT(*) FROM `books` WHERE `books`.`author_id` = 3
(0.4ms) SELECT COUNT(*) FROM `books` WHERE `books`.`author_id` = 4
(0.4ms) SELECT COUNT(*) FROM `books` WHERE `books`.`author_id` = 5
Author Load (0.6ms) SELECT `authors`.* FROM `authors`
今回はactiverecourd-precountを利用してN+1カウントクエリを解消する方法について紹介します。
目次
今回利用するサンプルについて
1:多で紐づいたAuthorモデルとBookモデルを例に、N+1カウントクエリの解消方法を紹介します。
activerecourd-precounterについて
activerecord-precounterはeager loadingによるキャッシュをcountクエリにも適用できるように拡張するgemです。
Rails標準のcounter_cacheを利用してN+1カウントクエリを解消する際はカラムを追加が必要です。
しかしactiverecourd-precounterの場合はカラムを追加することなくN+1カウントクエリを解消できます。
activerecord-precounterはactiverecord-precountの後継gemです。現在はactiverecord-precounterの利用が推奨されています。
activerecourd-precounterの使い方
activerecord-precounterの使い方について紹介します。
Gemの追加
Gemfile
gem 'activerecord-precounter'
$ bundle
コードの修正
以下のように修正します。
authors = Author.all
+ ActiveRecord::Precounter.new(authors).precount(:books)
authors.each do |author|
- author.books.count
+ author.books_count
end
動作確認
ログを確認するとN+1カウントクエリが発生していないことがわかります。
Author Load (0.5ms) SELECT `authors`.* FROM `authors`
(0.6ms) SELECT COUNT(*) AS count_all, `books`.`author_id` AS books_author_id FROM `books` WHERE `books`.`author_id` IN (1, 2, 3, 4, 5) GROUP BY `books`.`author_id`
まとめ
N+1カウントクエリを解消する方法としてcounter_cacheやcounter_cultureを利用する方法もありますが、どちらもカラムの追加が必要です。
一方、activerecord-precounterはマイグレーションをすることなくN+1カウントクエリを解消できるので導入が簡単です。
トリガでデータ更新をするのは実装を複雑化させてしまう要因にもなるため、N+1カウントクエリを解消するための有力候補はactiverecord-precounterだと感じています。
Twitter(@nishina555)やってます。フォローしてもらえるとうれしいです!