🔧

📝【Rails】クエリ最適化のためのexplainメソッド

2023/11/30に公開

explainメソッド

explainメソッドって何それ、おいしいの?

Active Record でのSQLクエリの実行計画を提供してくれるもの。クエリのパフォーマンスを理解し、最適化を求める人にとっては非常に美味しいメソッド

explainメソッドの実装コード ※1

explain メソッド内ではSQL文の生成や実行時間の計測、生成したSQL文によるクエリの実行、実行結果の整形処理を行っている

def explain(arel, binds = [])
  sql     = "EXPLAIN #{to_sql(arel, binds)}"              # 生成
  start   = Time.now
  result  = exec_query(sql, "EXPLAIN", binds)             # 実行
  elapsed = Time.now - start                              # 計測

  MySQL::ExplainPrettyPrinter.new.pp(result, elapsed)     # 整形
end

※1 source: https://github.com/rails/rails/blob/8b69e32412cc2867b5fdd9a33cf4e4e759057e95/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb#L178

使用方法

対象となるクエリチェーンの最後に追加する

User.where(name: 'Bob').explain

出力結果例(MySQL ※2)

// 上記のクエリの実行結果ではありません
+----+-------------+--------------------------+------------+--------+-----------------------------------------------------------------------------------------+--------------------------------------------+---------+--------------------------------------------------------------+------+----------+------------------------------------+
| id | select_type | table                    | partitions | type   | possible_keys                                                                           | key                                        | key_len | ref                                                          | rows | filtered | Extra                              |
+----+-------------+--------------------------+------------+--------+-----------------------------------------------------------------------------------------+--------------------------------------------+---------+--------------------------------------------------------------+------+----------+------------------------------------+
|  1 | SIMPLE      | exhibit_patrol_histories | NULL       | range  | exhibit_patrol_histories_admin_user_id_index,exhibit_patrol_histories_patroled_at_index | exhibit_patrol_histories_patroled_at_index | 6       | NULL                                                         |    1 |    100.0 | Using index condition; Using where |
|  1 | SIMPLE      | admin_users              | NULL       | eq_ref | PRIMARY                                                                                 | PRIMARY                                    | 8       | GameTrade_development.exhibit_patrol_histories.admin_user_id |    1 |     12.5 | Using where                        |
+----+-------------+--------------------------+------------+--------+-----------------------------------------------------------------------------------------+--------------------------------------------+---------+--------------------------------------------------------------+------+----------+------------------------------------+
2 rows in set (0.00 sec)

※2 explain メソッドによる出力はデータベースごとに異なる

各フィールドの読み方(重要なものだけを表示 ※3)

type
対象のテーブルに対してどのような方法でアクセスするか

  • const
    • PRIMARY KEY または UNIQUE インデックスによる単一行の参照で、最も効率が良い
    • PRIMARY KEY や UNIQUE インデックスに基づいて特定の値を指定するクエリで使用されることがある
  • eq_ref
    • JOIN で PRIMARY KEY または UNIQUE KEY を利用した一つの行の参照
    • テーブル同士の一対一の結合で使用されることがある
  • ref
    • インデックスを使用して等価検索(WHERE key = value)を行い、複数の行が存在する可能性のある行を選択
    • 外部キーがある場合や、一意でないインデックスを持つカラムでの検索クエリで使用されることがある
    • 検索条件を調整することで一致する行の数を減らし、クエリの効率を改善することが可能
  • range
    • インデックスを使用して特定の範囲検索
    • BETWEEN<> などの演算子を使用しているクエリで使用されることがある
    • フルテーブルスキャンよりも効率的であることが多いが、範囲が広すぎる場合はパフォーマンスに影響を与える可能性がある
  • ALL
    • フルテーブルスキャン。インデックスが全く利用されていない
    • 一般的に効率が悪く、小さいテーブルでない限りパフォーマンスに影響を及ぼす可能性がある

possible_keys
使用可能なインデックスの候補リスト

key
実際に使用するインデックス(NULL はインデックスが使用されていないことを意味する)

Extra
クエリがどのように実行されるかに関する追加情報

  • Using index
    • 必要なデータが全てインデックスから直接取得され、テーブルの行自体を読み込む必要がないことを示す
  • Using where
    • WHERE 句がクエリに使用されたことを示す
  • Using temporary
    • クエリの実行に一時テーブルが使用されていることを示す
    • ソートや集計(ex. GROUP BY, ORDER BY)のクエリで見られる

ref
検索条件で、インデックスのキーが参照(比較)されるカラムまたは定数。定数が指定されている場合は const 、JOINが実行されている場合には、結合先のテーブルで検索条件として利用されているカラムが表示される

※3 参考になる記事: https://nippondanji.blogspot.com/2009/03/mysqlexplain.html

explainメソッドをcountメソッドの後には実行できなかった件【余談】

結論

count メソッドにだけ実行できなかったのではなく、クエリが発行するメソッドの後にはそもそも explainメソッドが実行できない

サンプルコード

p Exhibit.where(sales_status: 1).count.explain

発生したエラー

undefined method `explain' for 5396:Integer

原因

.explain の実行対象が、 .count ですでにクエリが発行された結果(Integer)に対して実行してしまっているのでエラーが発生する

irb(main)> p Exhibit.where(sales_status: 1).count.class
Integer

Discussion