【アドベントカレンダー2025】クエリチューニングの最初の一歩~実行計画を見てみよう~

はじめに

こんにちは。開発部の國友です。

普段はデータベース管理者の業務を担当しています。

アプリケーションのパフォーマンス改善に取り組む際、「クエリが遅い」と感じたらまず行うべきことがあります。それは、RDBMSがそのクエリをどう実行しているか、その実行計画を確認することです。

皆さんは実行計画の確認方法、そして読み方を理解されていますか?

よく見るテーマではありますが、基礎だからこそ具体的な事例を通じて改めて実行計画を再確認しましょう。

実行計画とは

実行計画とはRDBMSがユーザーから受け取った SQL クエリを最も効率よく処理するために立てる、具体的な手順書です。

  1. ゴールは一つ

    ユーザーが書く SQL は目的を示しています。例えば、"SELECT * FROM employees WHERE emp_no > 400000"は、「従業員番号が 400000 より大きいユーザーを全部見つけてきてほしい」という目的を示しています。

  2. ルート(実行計画)は複数ある

    この目的を達成する方法(ルート)は複数存在します。

    • ルートA (非効率)

      テーブルの先頭から全行を端まで見ていく(フルスキャン)。

    • ルートB (効率的)

      emp_no カラムに貼られたインデックスを使って、該当するレコードの場所まで一気に飛ぶ。

  3. オプティマイザがルートを決める

    RDBMS内部のオプティマイザが統計情報や設定を元にコスト(時間やリソース)が最も低くなるベストなルート(実行計画)を決定し、それに基づいてクエリを実行します。

なぜ実行計画を見る必要があるのか?

オプティマイザは賢いですが、常に最良の判断をするとは限りません。

  • インデックスの確認

    適したインデックスが存在しない場合や、存在してもオプティマイザが利用しないと判断した場合、フルスキャンのような非効率なルートを選んでしまうことがあります。

  • ボトルネックの特定

    クエリのどの部分(どのテーブルアクセス、どのソート処理)に時間がかかっているかを客観的に把握し、ピンポイントで修正(インデックス追加、SQLの見直し)を行うことができます。

今回使用するDB

この記事の実例ではMySQL 8系を使用します。

MySQLから提供されているサンプルデータベースのemployeesを利用しています。

https://github.com/datacharmer/test_db

(実在の店舗データや社内データは一切含まれていません)

1. 実行計画の確認方法

クエリの実行計画を見るにはEXPLAINを使用します。

EXPLAINの使い方はとっても簡単です。分析したい SELECT クエリの前に付けるだけで実行できます。

構文

EXPLAIN SELECT 列名 FROM テーブル名 WHERE ...;

結果のサンプル

employees テーブルをフルスキャンするクエリの実行結果を見てみましょう

mysql> explain select * from employees\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: employees
   partitions: NULL
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 299645
     filtered: 100.00
        Extra: NULL
1 row in set, 1 warning (0.00 sec)

上記のような結果が返ってきます。この中で、typekeyExtra の3つのカラムに注目します。

2. EXPLAINの結果を見てみよう

① type カラム

type カラムはMySQLがテーブルからデータを取得するためにどのような方法(アクセスタイプ)を使ったかを示します。これはクエリの性能を判断するうえで最も重要な指標です。

type の値 意味 評価 対策
ALL テーブルの全行をスキャンしている(フルスキャン)。 対策しよう WHERE句の列にインデックスを作成するなど。
range インデックスを使って特定の範囲をスキャンしている。 許容範囲 範囲が狭ければ良い。
ref インデックスを使って特定のキー値と一致する行を取得している。 良い感じ 理想的なインデックス利用。
const/eq_ref プライマリキーやユニークキーを使って単一行を特定している。 最高 最も効率的。

② key カラム

key カラムは、実際にデータ取得に使われたインデックス名を示します。

  • key の値が NULL の場合

    インデックスが使われていないことを示します。typeALL の場合は特にインデックスがないか、あっても利用する価値がないとオプティマイザが判断しています。

  • possible_keys に表示されているのに keyNULL の場合

    インデックス自体は存在するが、オプティマイザが使わない判断をしたことを意味します。

③ 追加の実行情報を示す Extra カラム

Extra カラムには、MySQLが実行計画を立てるうえでの追加情報や注意点が表示されます。ここに特定のメッセージが出たら、要注意です。

Extra の値 意味 対策
Using filesort 結果のソートにメモリやディスクを使っている。 ORDER BY句の列にもインデックスを追加する。
Using temporary クエリ処理のために一時テーブルを作成している。 GROUP BYDISTINCT句の見直し、インデックス追加を検討。
Using index カバリングインデックス。インデックスのデータだけで全ての情報が取得できた(テーブルアクセス不要)。 青信号(最も高速な実行方法の一つ)。

例:Using filesortが表示されるようなクエリ

mysql> EXPLAIN SELECT * FROM employees  WHERE last_name = 'Facello'  ORDER BY birth_date\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: employees
   partitions: NULL
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 299556
     filtered: 10.00
        Extra: Using where; Using filesort
1 row in set, 1 warning (0.00 sec)

employeesテーブルにはemp_no(従業員No)のプライマリインデックスが設定されています。

上記クエリでは下記の流れで処理が行われます。

  1. last_nameが「Facello」で一致するレコードを見つけます。
  2. 一致したレコードはDBが格納されている順序(emp_no)でしか得られないためbirth_dateでソートします。

ソートし直しの際に結果をメモリやディスク上にコピーしてbirth_dateによる並び替えを実行する必要があるためUsing filesortと表示されます。

3. サブクエリ実行時のEXPLAINの読み方

サブクエリ(副問い合わせ)を含むクエリを EXPLAIN にかけると、結果が複数行に分かれます。このとき、どの行がどのクエリに対応しているかを理解するために、idselect_type カラムに注目する必要があります。

id カラムで実行順序を把握する

id カラムは、クエリ全体における処理の単位を示します。

  • id大きい数字の行から先に実行される(最も内側のサブクエリ)。
  • id同じ数字の行は、基本的に結合処理(JOIN)として実行される。

select_type カラムでサブクエリの種類を判別する

select_type カラムは、その行のクエリがどのような役割を持っているかを分類します。

select_type の値 意味 注意点
PRIMARY サブクエリを含むクエリ全体の最も外側の SELECT。
SUBQUERY WHERE 句や SELECT 句にある、非相関サブクエリ(外側のクエリに依存しない)。 メインクエリの実行前に一度だけ実行される。
DEPENDENT SUBQUERY 相関サブクエリ(外側のクエリの行ごとに実行される)。 注意が必要。繰り返し実行されるため、パフォーマンスのボトルネックになりやすい。
DERIVED FROM 句で定義された派生テーブル メインクエリの実行前に一時テーブルとして作成される。

検証例:相関サブクエリ(DEPENDENT SUBQUERY)

employees DBの employees テーブルと salaries テーブルを使い、各従業員の直近の給与を相関サブクエリで取得してみます。

mysql> EXPLAIN
    -> SELECT
    ->    e.emp_no,
    ->    (SELECT s.salary FROM salaries s
    ->     WHERE s.emp_no = e.emp_no
    ->     ORDER BY s.to_date DESC LIMIT 1) AS latest_salary
    -> FROM employees e
    -> LIMIT 10\G
*************************** 1. row ***************************
           id: 1
  select_type: PRIMARY
        table: e
   partitions: NULL
         type: index
possible_keys: NULL
          key: lastname_birthday
      key_len: 69
          ref: NULL
         rows: 299556
     filtered: 100.00
        Extra: Using index
*************************** 2. row ***************************
           id: 2
  select_type: DEPENDENT SUBQUERY
        table: s
   partitions: NULL
         type: ref
possible_keys: PRIMARY
          key: PRIMARY
      key_len: 4
          ref: employees.e.emp_no
         rows: 9
     filtered: 100.00
        Extra: Using filesort
2 rows in set, 2 warnings (0.00 sec)
  • id: 1 (PRIMARY)

    外側のメインクエリです。ここでは employees テーブルを読み込んでいます。

  • id: 2 (DEPENDENT SUBQUERY)

    内側のサブクエリです。この select_type が出た場合、外側のクエリの読み込んだ行数分だけ、このサブクエリが繰り返し実行されることを意味します。

従業員(emp_no)に対して毎回サブクエリ(最新の給与を取得する)が実行されます。従業員全員を対象とすると、この非効率性からパフォーマンスが大幅に低下します。

サブクエリを使う場合は、DEPENDENT SUBQUERY になっていないかを確認し、可能であればJOIN等を使って別の効率的な書き方に変換できないかを検討しましょう。

まとめ

MySQLのクエリパフォーマンス改善は、EXPLAINの結果を見て、判断して、改善するの繰り返しです。

  • typeALLrange ならインデックス設定など改善の余地あり。
  • keyNULL ならインデックス設計を見直す。
  • ExtraUsing filesortUsing temporary が出たらクエリ設計を見直す必要あり。
  • サブクエリでは select_type: DEPENDENT SUBQUERY を避ける。

これらを改善すれば、クエリは劇的に速くなる可能性があります。 是非今実行されているクエリを見直してみてください。

最後までお読みいただきありがとうございました!


楽器演奏が趣味。データベース運用が主な業務です。