この記事は 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をひとつのクエリで投げるのでここで期待した効果は得られなくなりました。