📘

【データベース(MySQL)】~ 実行計画について理解する ~

に公開

はじめに

インターンでデータベースの操作やパフォーマンスチューニングをする機会があり、そこで改めてデータベースってどうやって動いているんだっけ??となり、調べたのでまとめました!

なぜ記事書くの?

データベースには何を求めているかはSQLクエリを通して伝えることが多いですが、どうやって取得するかはなかなか意識することがなかったから!
これを機にデータベースを鮮明に見れるようになるため!

データにたどり着くまで

  1. SQLパーサ
    SQLクエリを解釈して機械で理解できるようにする。

  2. 統計情報
    テーブルの行数やインデックスに含まれる値、種類、分布を保持。
    オプティマイザがこれを参照し、最終的な経路を判断する。

  3. オプティマイザ
    SQLを実行するのに最適な経路を決める!
    統計情報を参照したり、コスト(速さ)を見積もったりして最終的な経路を決定する。
    完璧ではなく、統計情報に左右されて誤った判断をする可能性もある。

  4. 実行エンジン
    オプティマイザが決めた手順を実行する!

  5. ストレージエンジン
    データの管理と読み書きをする。
    インデックスアクセス、テーブルアクセスを実行しデータにたどり着く。

実行計画を読み解く

準備

  • 実行計画
    実行計画とは、SQLを実行するために選んだ手順のこと!
    どのテーブルをどの順番で読むか、インデックス使うか、JOINの方法は何か、などが書かれている。
    オプティマイザが決定して、実行エンジンが実行する。

  • EXPLAIN ANALYZE
     このコマンドは、実行計画の概要と実際にかかった時間などを表示する。

実行計画を読んでみる

githubにリポジトリを立ててローカルで試せるように準備しました!
dockerを使用してコンテナを立てています。ぜひ試してみてください!
https://github.com/ahiru401066/practice-SQL

最初に2025-09-11のデータを取得するSQLクエリを試しました。
EXPLAIN ANALYZEは以下のような感じ。
SQLクエリ

EXPLAIN ANALYZE
SELECT id, name, created_at
FROM users
WHERE created_at BETWEEN '2025-09-11 00:00:00' AND '2025-09-11 23:59:59';

読み解いてみる1

Table scan on users  (cost=9826 rows=96902) (actual time=0.0966..17.6 rows=100000 loops=1)
  • Table scan:テーブルをフルスキャン(全部調べる)することを選択
  • rows=96902:オプティマイザによる読み取り予測
  • actual time=0.0966..17.6 rows=100000
    最初の1行を返すまでが0.0966msで、全ての行を読み終えるまでが17.6ms。今回はusersテーブルの100000行をスキャンしている。

読み解いてみる2

Filter: (users.created_at like '2025-09-11%')  (cost=9826 rows=10766) (actual time=0.571..48.4 rows=270 loops=1)
  • Filtercreated_atで絞り込み
  • rows=10766:オプティマイザによる読み取り予測
  • actual time=1.36..75.1 rows=270:実際の時間と読み取った行

インデックスを追加してみる

フルスキャンは効率が悪いですね。
インデックスを追加してどう変化するかみてみましょう!

-- インデックス追加
CREATE INDEX idx_created_at ON users(created_at);

-- インデックスが追加されたか確認
SHOW INDEXES FROM users;

-- インデックスの削除
DROP INDEX idx_created_at ON users;

実行計画を読んでみる(インデックス追加後)


読み解いてみる

Index range scan on users using idx_created_at over ('2025-09-11 00:00:00' <= created_at <= '2025-09-11 23:59:59'), with index condition: (users.created_at between '2025-09-11 00:00:00' and '2025-09-11 23:59:59')  (cost=122 rows=270) (actual time=0.178..1.14 rows=270 loops=1)
  • Index range scan on users using idx_created_at over(...):インデックスを使用し、range scan(指定範囲を読み取り)をしている
  • with index condition: (users.created_at between ...):インデックスの段階で条件を絞って、無駄な読み込みを防ぐ
  • actual time=0.178..1.14 rows=270実際の時間は1.14msかかった

結果

実行時間
インデックス追加前 48.4ms
インデックス追加後 1.14ms

かなり早くなりましたね。
今回実行計画を見ることでどうして早くなったのか、まで理解することができます。
実行計画よりテーブルの全件スキャン -> インデックス範囲スキャンになったことで、不要な行アクセスが削減されたことが処理時間減少だとわかります。

アクセス方式の種類について

様々なアクセス方式があります。

  • Table scan:テーブル全件読む
  • Index scan:テーブル全件をインデックス経由で読む
  • Index lookup:インデックスを使って複数行を取得
    ...

最後に

データベースは深いですね...。
アプリケーション側からSQLで制御するとしても、データベースの仕組みを頭に入れておくことで素敵なシステムを構築したいです。
最後まで読んでいただきありがとうございました!



これから

  • データベース同士の比較
    MySQL, PostgreSQL,...の違いやそれぞれの良さを理解したい。
  • SQLパーサ、実行エンジン、...
    概要はなんとなくわかるけど、まだ理解足りないから深めたい...

Discussion