LoginSignup
30
25

More than 5 years have passed since last update.

N個のActiveRecordオブジェクトのそれぞれのassociationのCOUNTを出す方法4つの比較

Last updated at Posted at 2014-09-17

N個ActiveRecordのオブジェクト(モデルA)があったとして、それらが持つassociation(モデルB)のデータは要らずcountだけ表示したいとする。
その際、以下のような4つの手が思いつく。

  • AのテーブルにBの数をキャッシュするカラムを追加し、それをSELECTする
    • belongs_tocounter_cacheオプション
  • N回BのCOUNTクエリを走らせる
    • 普通にやるとこうなる
  • LEFT JOINGROUP BY A.idB.idをCOUNTする
    • がんばってクエリを1回にするとこうなる
  • N個のA.idからB.a_idをpluckしてきて各idごとの個数をとる
    • これをやる方法がないので作った: k0kubun/activerecord-has_count
    • count専用のhas_count associationを生やし、外部キーをpluckする専用のpreloaderを使うことができる
    • 作ったばかりでいまのところ最新のRailsでしかテストしてないので注意

カラムを好きに増やせる状況ではcounter_cacheを使えば良いが、問題はそうでない場合に残り3つのどれを選択すべきか、である。

ベンチをとる

「推測するな、計測せよ」というわけで、実際にベンチをとってみた。
DBはMySQLを使い、以下のコードでベンチをとった。

TWEET_COUNT = 5   # N個のレコード
REPLY_COUNT = 100 # COUNTしたい数

TWEET_COUNT.times do
  tweet = Tweet.create(replies_count_cache: 0)

  replies = REPLY_COUNT.times.map do
    Reply.create(tweet: tweet)
  end
end

Benchmark.bm do |bench|
  bench.report("counter_cache") do
    Tweet.first(TWEET_COUNT).map{ |t| t.replies_count_cache }
  end

  bench.report("N+1 COUNT query") do
    Tweet.first(TWEET_COUNT).map{ |t| t.replies.count }
  end

  bench.report("LEFT JOIN") do
    # 無駄にオブジェクト作りたくないのでincludesやeager_loadのLEFT JOINに頼らない
    tweets = Tweet.joins('LEFT JOIN replies ON tweets.id = replies.tweet_id').
      select('tweets.*, COUNT(replies.id) AS replies_count').
      group('tweets.id').first(TWEET_COUNT).
      map{ |t| t.replies_count }
  end

  bench.report("pluck foreign key") do
    # replies_count は activerecord-has_count gem 独自のhas_countというassociationで、
    # preloadするとActiveRecord内部で外部キーをpluckする
    Tweet.preload(:replies_count).first(TWEET_COUNT).map{ |t| t.replies_count }
  end
end

いろいろ省略してるけど要点は伝わったと思う。

結果

目的はcounter_cache以外の3つの方法を比較することだが、参考までにcounter_cacheの結果も並べる。

N = レコードの個数 TWEET_COUNT
C = COUNT対象の数 REPLY_COUNT

とすると、

N / C 5 / 10 5 / 100 5 / 1000 10 / 10000 1000 / 5
counter_cache(参考) 0.002101 0.001904 0.002094 0.010339 0.064779
N+1 COUNT query 0.008318 0.007874 0.009034 0.039144 0.554877
LEFT JOIN 0.004635 0.005358 0.006857 0.056636 0.497913
pluck foreign key 0.002272 0.004111 0.023250 0.616233 0.371234

となった。

どの方法を使うべきか

場合によって最速の方法が変わる結果となった。

傾向としては、以下のようなことがわかる。

  • 当たり前だけどcounter_cacheはいつでも速い
  • COUNT対象が 100オーダー以下 の場合、 外部キーをpluckするのが最速
    • クエリ効率が良く、数が小さければRubyでやる処理が少ないので速い
  • COUNT対象が 1000オーダー の場合、LEFT JOINするのが最速
    • たいていの場合無難な速さが出る
  • COUNT対象が 10000オーダー以上の場合、COUNTクエリをN回走らせるのが最速
    • JOINコストが大きくなり、COUNTしまくるほうが速くなる
    • あんまこういうケースなさそう

結論

カラムを追加しても問題ないときはcounter_cacheを使う。
そうでなければ、COUNT対象があまり多くないときはactiverecord-has_countを使い、ある程度スケールしてきたらLEFT JOINを行うのが良い。

30
25
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
30
25