#09 SQLをちゃんと理解する

インデックスと実行計画の基礎

インデックスとは

インデックス? テーブルの検索を高速化するためのデータ構造。本の索引のように、特定の列の値から行の位置をすばやく特定できる。検索は速くなるが、書き込みはやや遅くなる。 とはデータを素早く検索するための「索引」です。本の巻末索引と同様に、特定の値がどこにあるかを素早く見つけられるようにします。

インデックスがない場合、データベースはテーブルの全行を順番にスキャン(フルテーブルスキャン)します。100万行のテーブルでも全行を調べる必要があります。

インデックスがある場合は、B-treeを辿ることで対数時間(O(log n))で目的のデータを見つけられます。


B-tree インデックスの構造

B-treeインデックス? MySQLのデフォルトのインデックス構造。バランス木(Balanced Tree)で値を管理し、等値検索・範囲検索・ORDER BY の高速化に有効。ほとんどのユースケースでこれを使う。 はほとんどのRDBMSで使われているデフォルトのインデックス構造です。

B-tree インデックスの構造 ルートノード 50 | 150 ブランチ(< 50) 10 | 30 ブランチ(50〜149) 70 | 100 ブランチ(>= 150) 180 | 200 リーフ 1→p1, 5→p2 pN=行ポインタ リーフ 10→p3, 20→p4 リーフ 30→p5, 40→p6 リーフ 150→p7 リーフ 200→p8 B-tree のポイント • ルート→ブランチ→リーフを辿って目的のキーを発見 • 1000万行でもツリーの高さは約23段(O(log n)) • リーフは隣接リーフへのポインタを持つ(範囲検索に有効) 検索の流れ(id = 20を検索) ① ルート: 20 < 50 → 左ブランチへ ② ブランチ: 20 > 10 → 次のリーフへ ③ リーフ: 20→p4 → 行データを取得!
図1: B-treeインデックスの構造(ルート→ブランチ→リーフ)

B-treeは「バランスドツリー」の略で、次の構造を持ちます。

  • ルートノード: ツリーの頂点。検索はここから始まる
  • ブランチノード: 中間ノード。キーの範囲で左右に分岐
  • リーフノード: 末端。実際のキー値と行の物理位置(ポインタ)を格納
-- 検索が O(log n) で済む理由
-- 1000万行のテーブルでも、B-treeの高さは約23程度
-- → 23回の比較で目的の行を見つけられる

インデックスの作成

-- 単一列インデックス
CREATE INDEX idx_orders_customer ON orders (customer_id);

-- 複合インデックス
CREATE INDEX idx_orders_date_status ON orders (created_at, status);

-- ユニークインデックス
CREATE UNIQUE INDEX idx_users_email ON users (email);

-- 削除
DROP INDEX idx_orders_customer ON orders;  -- MySQL
DROP INDEX idx_orders_customer;            -- PostgreSQL

主キーには自動的にユニークインデックスが作成されます。


EXPLAIN の読み方

EXPLAIN を付けてクエリを実行すると、データベースがどのように処理するかの実行計画が得られます。

EXPLAIN の出力の読み方 id | select_type | table | type | possible_keys | key | rows | Extra 1 | SIMPLE | orders | ref | idx_cust,idx_dt| idx_customer | 150 | Using where type: アクセス種別 ref = インデックスで等値検索 最重要カラム! key: 使用インデックス NULL → インデックス未使用 → 遅い可能性あり rows: 推定スキャン行数 小さいほど良い 実際の値ではなく推定値 type の速さランキング(左ほど速い) const eq_ref ref range index ALL (フルテーブルスキャン: 最も遅い) Extraカラムの主な値 Using index カバリングインデックス(高速) Using where WHERE条件でフィルタ中 Using filesort ソートにインデックス未使用 Using temporary 一時テーブル使用(要改善) Using index condition インデックスコンディションプッシュ 改善のチェックリスト □ type=ALL → インデックスを追加 □ key=NULL → WHERE句の条件を見直し □ Using filesort → ORDER BYにインデックス □ rows が多い → インデックスの絞り込み力確認 □ Using temporary → クエリ構造を見直し
図2: EXPLAINの出力の各カラムが意味すること
EXPLAIN SELECT * FROM orders
WHERE customer_id = 101
AND status = 'paid';

MySQL EXPLAINの主要カラム

+----+-------------+--------+------+---------------+---------+---------+-------+------+-------+
| id | select_type | table  | type | possible_keys | key     | key_len | ref   | rows | Extra |
+----+-------------+--------+------+---------------+---------+---------+-------+------+-------+
|  1 | SIMPLE      | orders | ref  | idx_customer  | idx_cus | 4       | const | 150  | Using where |

type(アクセスタイプ): パフォーマンスへの影響が最も大きい。良い順に:

type意味速さ
const主キーで1行を特定最速
eq_refJOINで主キー/ユニークキーを使用非常に速い
refインデックスを使った等値検索速い
rangeインデックスを使った範囲検索まあまあ
indexインデックス全体のスキャン遅い
ALLフルテーブルスキャン最遅

key: 実際に使われたインデックス名(NULLならインデックス未使用)

rows: スキャンが必要な行数の推定値(小さいほど良い)

Extra: 追加情報。Using filesort(ソートでインデックス未使用)や Using temporary(一時テーブル使用)が出たら要改善

-- PostgreSQL の場合
EXPLAIN ANALYZE SELECT * FROM orders WHERE customer_id = 101;
-- ANALYZE を付けると実際の実行時間も表示

インデックスが効くケース・効かないケース

効くケース

-- 等値検索
WHERE customer_id = 101

-- 範囲検索
WHERE created_at >= '2025-01-01'

-- LIKE の前方一致
WHERE name LIKE '田中%'

-- ORDER BY(インデックスの並び順と一致する場合)
ORDER BY customer_id

-- JOIN の結合キー
ON orders.customer_id = customers.id

効かないケース

-- 列に関数を適用
WHERE YEAR(created_at) = 2025  -- インデックス効かない
-- 代わりに:
WHERE created_at >= '2025-01-01' AND created_at < '2026-01-01'

-- LIKEの後方一致・中間一致
WHERE name LIKE '%田中'   -- インデックス効かない
WHERE name LIKE '%田中%'  -- インデックス効かない

-- 演算で列を変形
WHERE price * 1.1 > 1000  -- インデックス効かない
-- 代わりに:
WHERE price > 1000 / 1.1

-- NULLの検索(DBMSによる)
WHERE col IS NULL  -- DBMSによってはインデックス効かない

-- 低カーディナリティの列(例: status が3種類しかない)
WHERE status = 'paid'  -- 全体の50%が該当する場合はフルスキャンの方が速い

複合インデックスの列順

複合インデックスは列の順序が非常に重要です。

CREATE INDEX idx_orders_customer_date ON orders (customer_id, created_at);

このインデックスが効くクエリと効かないクエリ:

-- OK: 先頭列(customer_id)を使っている
WHERE customer_id = 101
WHERE customer_id = 101 AND created_at >= '2025-01-01'

-- NG: 先頭列をスキップしている(created_at単独はインデックス効かない)
WHERE created_at >= '2025-01-01'  -- インデックス効かない

最左プレフィックスルール: 複合インデックスは先頭列から順番に使われます。先頭列をスキップした条件にはインデックスが効きません。

設計のヒント:

  1. WHERE句でよく使う列を先頭に
  2. 等値検索の列を範囲検索の列より前に
  3. カーディナリティ(種類数)が高い列を前に

カバリングインデックス

クエリが必要とするすべての列がインデックスに含まれている場合、テーブル本体へのアクセスが不要になります。これをカバリングインデックスと言います。

-- クエリ
SELECT customer_id, SUM(amount)
FROM orders
WHERE customer_id = 101
GROUP BY customer_id;

-- カバリングインデックスを作成
CREATE INDEX idx_covering ON orders (customer_id, amount);
-- customer_id と amount だけでクエリが完結できる
-- EXPLAINの Extra に "Using index" と表示される(テーブルアクセスなし)

まとめ

  • インデックス(B-tree)によって検索がO(log n)に高速化される
  • EXPLAIN で実行計画を確認し typekeyrowsExtra を見る
  • type=ALL(フルスキャン)や Using filesort が出たら改善を検討
  • 関数適用・後方LIKE・演算変形ではインデックスが効かない
  • 複合インデックスは最左プレフィックスルールに従い列順が重要
  • カバリングインデックスでテーブルアクセスを完全に省略できる

シリーズを終えて

全9話の「SQLをちゃんと理解する」シリーズ、お疲れ様でした。このシリーズでは SELECT文? テーブルからデータを取得するSQL文。`SELECT * FROM users` のように使う。WHERE で絞り込み、ORDER BY で並び替え、LIMIT で件数を制限できる。 の実行順序から始まり、 JOIN? 複数のテーブルを結合して一つの結果セットにする操作。INNER JOIN(両方に存在する行のみ)・LEFT JOIN(左テーブルの全行 + 一致した右テーブル)などがある。 サブクエリ? SQL文の中に入れ子になった別のSELECT文。WHERE句・FROM句・SELECT句などで使用できる。「平均より多い注文をしたユーザー」のような複雑な条件に使う。 CTE(共通テーブル式)? WITH句で定義する一時的な名前付きクエリ。`WITH monthly_sales AS (SELECT ...)` のように使い、複雑なサブクエリを読みやすく整理できる。再帰CTEでツリー構造も扱える。 GROUP BY? 指定した列の値が同じ行をまとめてグループ化する句。COUNT・SUM・AVGなどの集計関数と組み合わせて使う。「カテゴリ別の件数を集計」などに使用する。 ウィンドウ関数? OVER()句を使って、グループ化せずに集計・順位付けを行う関数。ROW_NUMBER()・RANK()・LAG()・SUM() OVER() など。GROUP BYと違い、元の行を保持したまま計算できる。 NULL? SQLにおける「値が存在しない」を表す特殊な状態。NULLは値ではないため、`= NULL` では比較できず、`IS NULL` / `IS NOT NULL` を使う。NULLを含む演算は基本的にNULLになる。 トランザクション? 複数のDB操作をひとまとまりとして扱う仕組み。途中でエラーが起きたら全部元に戻す(ロールバック)ことで、データの整合性を保つ。 インデックス? テーブルの検索を高速化するためのデータ構造。本の索引のように、特定の列の値から行の位置をすばやく特定できる。検索は速くなるが、書き込みはやや遅くなる。 まで、「なんとなく使えている」から「仕組みを理解して使える」へのステップを一緒に歩んできました。

次のステップとして以下をおすすめします:

  • 実際のプロジェクトで EXPLAIN を使ってクエリを最適化してみる
  • 正規化? データの重複をなくし、更新時の不整合を防ぐためにテーブルを分割する設計手法。第1〜第3正規形などの段階がある。整理されたDBはメンテナンスしやすくなる。 ER図? Entity-Relationship Diagram。エンティティ(テーブル)とその間の関係(リレーション)を視覚化した設計図。DBの構造を理解・共有するために使う。 を学んでテーブル設計を深める
  • ストアドプロシージャやビューなどの高度な機能を調べる
  • 使っているDBMS固有の機能(PostgreSQLのJSONB、MySQLのJSON関数など)を探る

SQLは書くほど力がつきます。ぜひ実際のデータで手を動かして続けてください!