かみぽわーる

kamipo's blog

ActiveRecordでINの中が一万個とかにならないようにする

この記事は MySQL Casual Advent Calendar 2017 の23日目の記事です。

みなさんORマッパーは使っていますか?

僕は仕事とか趣味でActiveRecordというORマッパーを使っているんですけど、こいつ例えば

Team.preload(players: :high_score).to_a

みたいなことをするとすぐ

SELECT `scores`.* FROM `scores` FROM `scores`.`id` IN (a, b, c, ...数千個続く...)

みたいなクエリを生成しよるんですけど、MySQL 5.7に上げたときに range_optimizer_max_mem_size の制限で実行計画がテーブルスキャンに落ちてえらい目にあったことがありました。MySQL側で range_optimizer_max_mem_size = 0 することでこの制限を無くすことができますが、MySQL側の設定によらずアプリケーションが動作できるようINの中の個数を制限する方法をここでは考えます。

MySQL以外のことはよくわからないんですが、どうやらOracleにはINの中の個数が1000個までしか入れられないという制限があるようで、これに対応するためにActiveRecordには in_clause_length の個数ずつにsliceしてクエリを投げる仕組みが元々存在します。Oracle以外には個数の制限はないんですけど、この値を上書きすると他のバックエンドでもINの中の個数を制限することができるわけです。例えばOracle同様1000個までに制限したい場合は以下のようにすればよいです。

ActiveSupport.on_load(:active_record) do
  module ActiveRecord
    module ConnectionAdapters
      module DatabaseLimitsExt
        def in_clause_length
          1000
        end
      end

      class AbstractAdapter
        prepend DatabaseLimitsExt
      end
    end
  end
end

もしみなさんのORマッパーがINの中が一万個とかになるクエリを生成しているなら range_optimizer_max_mem_size の制限に引っかかってないか確認してみるといいかもしれません。

【追記】

Rails 6.0.0以降sliceしたINをひとつのクエリで投げるのでここで期待した効果は得られなくなりました。

github.com