起きる問題
mysql8.0.23以降で、select count(*) が、パラレルスキャンによりもの凄い時間がかかるようになってしまうため、kaminariのpaginationで表示がpumaなどがtimeoutしてしまう。
そもそもの問題の詳細はこちら
原因の調査
kaminariではページ数をしらべるために、上記のクエリーを発行しています。
これはkaminariのAR拡張で実装されています。
def total_count(column_name = :all, _options = nil) #:nodoc:
return @total_count if defined?(@total_count) && @total_count
# There are some cases that total count can be deduced from loaded records
if loaded?
# Total count has to be 0 if loaded records are 0
return @total_count = 0 if (current_page == 1) && @records.empty?
# Total count is calculable at the last page
return @total_count = offset_value + @records.length if @records.any? && (@records.length < limit_value)
end
# #count overrides the #select which could include generated columns referenced in #order, so skip #order here, where it's irrelevant to the result anyway
c = except(:offset, :limit, :order)
# Remove includes only if they are irrelevant
c = c.except(:includes, :eager_load, :preload) unless references_eager_loaded_tables?
c = c.limit(max_pages * limit_value) if max_pages && max_pages.respond_to?(:*)
# .group returns an OrderedHash that responds to #count
c = c.count(column_name)
@total_count = if c.is_a?(Hash) || c.is_a?(ActiveSupport::OrderedHash)
c.count(count_name)
elsif c.respond_to? :count
c.count(column_name)
else
c
end
end
このメソッドの第1引数でcolumn_nameを指定できますが、total_pagesをするところでは、オプションをわたしていないため、常に:allがよばれる仕様になっているようです。そのため、total_pagesを表示すると、常に上記のクエリーが発行されてしまいます。
対策
viewでpaginateをよんでいるところで、total_pagesを明示的に指定されるところで上記メソッドにオプションをあたえて、idのみを取得し、limit_valueで割り算することにしました。現状これが一番分かり易い対応方法な感じがします。
= paginate @orders, total_pages: @orders.total_count(:id) / @orders.limit_value
これでも遅いとき
しょうがないからidの最後で割り算する
= paginate @orders, total_pages: Order.last.id / @orders.limit_value
多少あわないけど力業で動くので業務用ならこれでおっけ。
余談
-
APIからのresponseを全部保管するためtextのmedium_textをつかっているため、:allのパラレルスキャンが死ぬほどかかっているのではないかと推測します。
-
あとは、作成日でソートしようとおもってorder(created_at: :desc)とかしたときに、created_atにindexがされていないといった理由もあったので、order(id: :desc)にしました。