目次
N+1カウントクエリが発生している状態とは
N+1カウントクエリとは、取得したN件のデータそれぞれに対してcountクエリが発行される現象のことを指します。
以下はN+1カウントクエリの例です。取得したN件のauthorに対してcountクエリが実行されていることがわかります。
コード
Author.all.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`
今回はRailsの標準機能であるcounter_cacheを利用してN+1カウントクエリを解消する方法について紹介します。
今回利用するサンプルについて
1:多で紐づいたAuthorモデルとBookモデルを例に、N+1カウントクエリの解消方法を紹介します。
counter_cacheについて
counter_cacheはRailsの標準機能です。
集計対象のレコード数を集計用カラムに保存することでN+1カウントクエリを解消します。
counter_cacheの導入手順
counter_cacheの導入手順について紹介します。
集計対象のモデルの修正
集計対象のモデルにcounter_cache: true
を追加します。
今回の場合、Authorモデルに紐づくBookモデルの数をキャッシュするためBookモデルを修正します。
book.rb
class Book < ApplicationRecord
belongs_to :author, counter_cache: true
end
集計用カラムの追加
集計した数を保存するカラムを作成します。デフォルトのカラム名は[テーブル名]_count
です。
counter_cache: true
の代わりにcounter_cache: [カラム名]
とすることでカラム名を変更できます。
今回の場合はBookモデルにcounter_cache: true
を設定しているため、books_count
カラムをAuthorモデルに追加します。
$ rails g migration AddBooksCountToAuthors
xxx_add_books_count_to_authors.rb
class AddBooksCountToAuthors < ActiveRecord::Migration[6.0]
def change
add_column :authors, :books_count, :integer, null: false, default: 0
end
end
コードの修正
countクエリの代わりに新しく追加したbooks_count
カラムを利用します。
Author.all.each do |author|
- author.books.count
+ author.books_count
end
動作確認
ログを確認するとN+1カウントクエリが発生していないことがわかります。
Author Load (0.6ms) SELECT `authors`.* FROM `authors` ORDER BY `authors`.`id` ASC LIMIT 1000
Author Load (0.6ms) SELECT `authors`.* FROM `authors`
コンソールでレコードを作成すると、books_count
カラムにbooks.count
の値が保存されていることがわかります。
author = Author.new(name: "hoge")
author.save
author.books.count
=> 0
author.books_count
=> 0
author.books.create(title: "fuga")
author.books.count
=> 1
author.books_count
=> 1
counter_cache導入前のレコードは集計カラムに反映されないので注意
レコードが追加されるたびに集計用カラムの値が変更されます。
ただしcounter_cache導入前に集計対象のレコードがすでに存在していた場合、その数は集計用カラムに反映されません。
### counter_cache導入前に紐づいていた3つのbooksは反映されていない
author = Author.find(1)
author.books.count
=> 3
author.books_count
=> 0
### counter_cache導入後に追加されたレコードは反映される
author.books.create(title: "fuga")
author.books.count
=> 4
author.books_count
=> 1
counter_cache導入前のレコードを集計カラムに反映させるにはスクリプトを作ったりコンソールを実行したりする必要があります。
あるいはcounter_cultureの『集計カラムに集計数を反映させるメソッド』を利用する方法もあります。
counter_cultureを利用した集計数の反映方法は以下の通りです。
Gemfile
gem 'counter_culture'
book.rb
### counter_cacheの代わりにcounter_cultureでbooks_countカラムに集計数をキャッシュするように変更
class Book < ApplicationRecord
- belongs_to :author, counter_cache: true
+ belongs_to :author
+ counter_culture :author
end
### 集計数の反映前
author = Author.find(1)
author.books.count
=> 3
author.books_count
=> 0
### 集計数の反映実行
Book.counter_culture_fix_counts
### 集計数の反映後
author = Author.find(1)
author.books.count
=> 3
author.books_count
=> 3
まとめ
- 集計対象の子モデルにcounter_cacheオプションを追加する
- 集計用のカラムを親モデルに追加する
- countクエリの代わりに追加したcountカラムを利用する
Twitter(@nishina555)やってます。フォローしてもらえるとうれしいです!