QA@IT

ActiveRecordでINNER JOINの内部表側にFORCE INDEXする方法は?

6699 PV

MySQLのFORCE INDEXヒント構文を割と頻繁に使うので、lib/active_record_extension.rb

module ActiveRecordExtension
  extend ActiveSupport::Concern

  module ClassMethods
    def force_index(name)
      from("#{quoted_table_name} FORCE INDEX (#{name})")
    end
  end
end

という拡張を定義し、initializerから

ActiveRecord::Base.send(:include, ActiveRecordExtension)

とやって、

User.where(...).force_index(:index_users_on_created_at).order('created_at DESC').limit(10)

みたいなことをやったりしてるのですが、これが使えるのはジョインの駆動表が対象の場合だけで、逆にusersテーブルがjoinされる内部表側にある場合には(つまり取得対象が別テーブルの場合には)、

SELECT profiles.* FROM profiles INNER JOIN users FORCE INDEX (index_users_on_created_at) ON profiles.id = users.id ORDER BY created_at DESC LIMIT 10

のようなことができるスコープを定義したいのですが(users has_one profileを想定)、汎用的なうまい方法がうかびません。

なにか良い方法はあるでしょうか?

回答

あまり検証してませんが、こんなのでどうでしょう。
reflections からモデル間の関連に関する情報を取得して、SQL を組み立てています。

module ActiveRecordExtension
  module ClassMethods
    def join_with_force_index(relation_name, index_name)
      relation = reflections[relation_name]
      join_table_name = relation.klass.table_name
      c = connection
      joins <<-SQL
        INNER JOIN #{c.quote_table_name join_table_name}
        FORCE INDEX (#{c.quote_table_name index_name})
        ON #{c.quote_table_name table_name}.#{c.quote_column_name relation.foreign_key}
           = #{c.quote_table_name join_table_name}.#{c.quote_column_name relation.association_primary_key}
      SQL
    end
  end
end

下記のように使えます。

# Profile に belongs_to :user がある場合
Profile.where(...).join_with_force_index(:user, :index_users_on_created_at).order('created_at DESC').limit(10)
編集 履歴 (0)
  • 素晴らしい!バッチリ動きました。ありがとうございます! -
ウォッチ

この質問への回答やコメントをメールでお知らせします。