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
を組み合わせられない