Help us understand the problem. What is going on with this article?

Rails で MySQL インデックスヒントを使う (USE INDEX, IGNORE INDEX, FORCE INDEX)

More than 1 year has passed since last update.

MySQL を使った Rails で

Model.where(created_at: 1.yea.ago..1.day.ago).order(:id).last

などとすると、MySQL の仕様で created_at の INDEX ではなく、PRIMARY を使ってしまってものすごく遅くなることがあった。

調べてみたところ、どうやら自分で拡張するしかないようなので書いたという話です。

Concern

ググると類似コードが出てきますが、違いは下記の通り

  • カラム名で指定可能
  • 複合インデックスも[カラム名, カラム名]で指定可能
  • 普通にインデックス名もok
app/models/concerns/index_hint.rb
module IndexHint
  extend ActiveSupport::Concern

  class_methods do
    def convert_to_index_name_in_case_of_column_name(*names)
      names.map { |idx| find_index_name_by_colomun_names(idx) || idx }
    end

    private

    def find_index_name_by_colomun_names(column_names)
      column_names = Array.wrap(column_names).map(&:to_s)

      name = connection.indexes(table_name).find { |index| index.columns == column_names }&.name
      connection.quote_column_name(name) if name
    end
  end

  included do
    scope :use_index, lambda { |*indexes|
      index_names = convert_to_index_name_in_case_of_column_name(*indexes)
      from("#{quoted_table_name} USE INDEX(#{index_names.join(', ')})")
    }

    scope :ignore_index, lambda { |*indexes|
      index_names = convert_to_index_name_in_case_of_column_name(*indexes)
      from("#{quoted_table_name} IGNORE INDEX(#{index_names.join(', ')})")
    }

    scope :force_index, lambda { |*indexes|
      index_names = convert_to_index_name_in_case_of_column_name(*indexes)
      from("#{quoted_table_name} FORCE INDEX(#{index_names.join(', ')})")
    }
  end
end

Use

ApplicationRecord レベルで include してあげれば全てのモデルで使えますね。
モデル単体で include してもokです。

app/models/application_record.rb
class ApplicationRecord < ActiveRecord::Base
  include IndexHint
...

実際に使うとこんな感じ

Model.use_index(:created_at).to_sql
#=> "SELECT `models`.* FROM `models` USE INDEX(`index_models_on_created_at`)"

Model.force_index([:section, :name]).to_sql
#=> "SELECT `models`.* FROM `models` FORCE INDEX(`index_models_on_section_and_name`)"

Model.ignore_index([:section, :name], :created_at).to_sql
#=> "SELECT `models`.* FROM `models` IGNORE INDEX(`index_models_on_section_and_name`, `index_models_on_created_at`)"

注意点

  • from, use_index, force_index, ignore_index を組み合わせられない

参考

HAZI
主要言語は JavaScript, Ruby, Perl など。最近は Ruby 中心。 業務システム開発を主に行っている。その他、デザインなども。
http://hazi.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away