1. HAZI

    Posted

    HAZI
Changes in title
+Rails で MySQL インデックスヒントを使う (USE INDEX, IGNORE INDEX, FORCE INDEX)
Changes in tags
Changes in body
Source | HTML | Preview
@@ -0,0 +1,89 @@
+MySQL を使った Rails で
+
+```ruby
+Model.where(created_at: 1.yea.ago..1.day.ago).order(:id).last
+```
+
+などとすると、MySQL の仕様で `created_at` の INDEX ではなく、`PRIMARY` を使ってしまってものすごく遅くなることがあった。
+
+調べてみたところ、どうやら自分で拡張するしかないようなので書いたという話です。
+
+## Concern
+
+ググると類似コードが出てきますが、違いは下記の通り
+
+* カラム名で指定可能
+* 複合インデックスも`[カラム名, カラム名]`で指定可能
+* 普通にインデックス名もok
+
+```ruby: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(self.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("#{self.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("#{self.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("#{self.quoted_table_name} FORCE INDEX(#{index_names.join(', ')})")
+ }
+ end
+end
+```
+
+## Use
+
+`ApplicationRecord` レベルで `include` してあげれば全てのモデルで使えますね。
+モデル単体で `include` してもokです。
+
+```ruby:app/models/application_record.rb
+class ApplicationRecord < ActiveRecord::Base
+ include IndexHint
+...
+```
+
+実際に使うとこんな感じ
+
+```ruby
+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` を組み合わせられない
+
+## 参考
+
+* [\[Feature\] Active Record: Add \`use\_index\` by gaurish · Pull Request \#30514 · rails/rails](https://github.com/rails/rails/pull/30514)
+* [MySQL :: MySQL 5\.6 リファレンスマニュアル :: 13\.2\.9\.3 インデックスヒントの構文](https://dev.mysql.com/doc/refman/5.6/ja/index-hints.html)
+* [ruby on rails & mysql use force index \- Qiita](https://qiita.com/cut_yocchan_ika/items/98280f0b91b3af0aef84)