0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

rails + mysql8 + kaminariの時にpaginationでtimeoutする

Last updated at Posted at 2024-05-04

起きる問題

mysql8.0.23以降で、select count(*) が、パラレルスキャンによりもの凄い時間がかかるようになってしまうため、kaminariのpaginationで表示がpumaなどがtimeoutしてしまう。

そもそもの問題の詳細はこちら

原因の調査

kaminariではページ数をしらべるために、上記のクエリーを発行しています。
これはkaminariのAR拡張で実装されています。

kaminari-activerecord/lib/kaminari/activerecord/active_record_relation_methods.rb

    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)にしました。

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?