activerecourd-precounterでN+1カウントクエリを解消する手順

Ruby

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-precounteractiverecord-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)やってます。フォローしてもらえるとうれしいです!