はじめに
MySQLでクエリのパフォーマンスを調査する際にまず行うのが EXPLAIN
による実行計画の確認です。
しかし、インデックスを張っているにもかかわらず、意図したインデックスが使われていないというケースがあります。
この記事では、そういったケースに遭遇したときに、Rails(ActiveRecord)から MySQLの USE INDEX
を指定して、明示的にインデックスを使わせる方法を紹介します。
想定する状況
たとえば、Railsのマイグレーションで以下のように blogs テーブルに複数のインデックスを追加したとします。
add_index :blogs, :user_id
add_index :blogs, :category_id
これにより、次の2つのインデックスが作成されます。
index_blogs_on_user_id
index_blogs_on_category_id
この状態で以下のようなクエリを実行したとします。
Blog.where(user_id: 1024).order("category_id DESC")
このとき、Railsが生成するSQLは以下のようになります。
SELECT * FROM blogs WHERE user_id = 1024 ORDER BY category_id DESC;
このクエリでは user_id を条件にしていますが、EXPLAIN
を確認すると、なぜか index_blogs_on_user_id
ではなく index_blogs_on_category_id
が使われていたということがあります。
これは、MySQLが ORDER BY
に基づいた並び替えのコストを下げるために、category_id
のインデックスを優先して使おうとするためです。
しかし、検索条件の高速化を目的として user_id
のインデックスを使いたい場合には、この挙動が望ましくないということがあります。
.from を使って USE INDEX を指定する
MySQLでは、インデックスの使用を明示的に促すために USE INDEX
という構文が用意されています。
これをRailsで利用するには、ActiveRecordの .from
メソッドにSQL文字列を直接渡します。
Blog.from('blogs USE INDEX(index_blogs_on_user_id)').where(user_id: 1024).order("category_id DESC")
このとき、Railsが生成するSQLは以下のようになります。
SELECT * FROM blogs USE INDEX(index_blogs_on_user_id) WHERE user_id = 1024 ORDER BY category_id DESC;
このように、MySQLに対して意図したインデックスの使用を促すことができます。
注意点
USE INDEX
はインデックスの「使用を促す」ヒントであり、実際に使用されるかどうかはMySQLのクエリオプティマイザの判断に委ねられます。
FORCE INDEX を使ってインデックス使用を強制する
どうしても特定のインデックスを使わせたい場合は、USE INDEX
ではなく FORCE INDEX
を使うこともできます。
Blog.from('blogs FORCE INDEX(index_blogs_on_user_id)').where(user_id: 1024).order("category_id DESC")
これは、MySQLに対してそのインデックスを「強制的に使え」と指示するものです。
ただし、強制することで逆にパフォーマンスが悪化する可能性もあるため、慎重に使用する必要があります。
おわりに
EXPLAIN
を使ってクエリのチューニングをしていると、本来使われるべきインデックスが使われていないことが少なからずあります。
そうした場面では、Railsでも .from(... USE INDEX(...))
や .from(... FORCE INDEX(...))
を使うことで対処できる場合があります。
パフォーマンス改善の選択肢の一つとして、ActiveRecordにおけるインデックス指定の手段を知っておくと有効な場面が出てくるでしょう。