#03 データベースの高速化

B-treeインデックスの仕組み

「インデックスを貼れば速くなる」という話は知っていても、なぜ速くなるのかを説明できる人は意外と少ないものです。仕組みを理解すると、どのカラムにインデックスを貼るべきか、なぜ効かない場合があるのかを自分で判断できるようになります。

B-tree インデックスの構造 ルートノード [ 40 | 70 ] 内部ノード [ 10 | 25 ] [ 50 | 60 ] [ 75 | 90 ] リーフノード (データへの ポインタ) 1,5,8 →row 10,20 →row 30,35 →row 42,48 →row 50,58 →row 62,68 →row 72,74 →row 80,85 →row 92,99 →row ← リーフノードは双方向リンクで繋がっている(範囲検索を効率化) 検索の効率:O(log n) 100万行のテーブルでも約20回の比較で目的のデータに到達できる (log₂(1,000,000) ≈ 20)
図1: B-treeの構造

B-tree とは

B-treeインデックス? MySQLのデフォルトのインデックス構造。バランス木(Balanced Tree)で値を管理し、等値検索・範囲検索・ORDER BY の高速化に有効。ほとんどのユースケースでこれを使う。 は「バランス木(Balanced Tree)」の略で、MySQL(InnoDB)が インデックス? テーブルの検索を高速化するためのデータ構造。本の索引のように、特定の列の値から行の位置をすばやく特定できる。検索は速くなるが、書き込みはやや遅くなる。 の実装に使っているデータ構造です。

木構造の特徴は検索の効率にあります。100万行のテーブルを線形探索(端から1件ずつ確認)すると最大100万回の比較が必要ですが、B-treeを使うと約20回の比較で見つかります。これは log₂(1,000,000) ≈ 20 という対数の性質によるものです。

B-treeの構造

B-treeは以下の3種類のノードで構成されます。

  • ルートノード:木の根。検索はここから始まる
  • 内部ノード:分岐点。キー値と子ノードへのポインタを持つ
  • リーフノード:末端。実際のデータへのポインタを持ち、互いにリンクされている

リーフノードが双方向にリンクされている点が重要です。これにより範囲検索が効率的になります。

ルート:     [50]
           /    \
内部:   [20,30]  [70,90]
        /  |  \    /  |  \
リーフ: [10,15] [20,25] [30,35] ...(双方向リンク)

インデックスの物理構造(InnoDB)

InnoDB では2種類のインデックス構造があります。

クラスタインデックス(プライマリキー)

InnoDB のテーブルはプライマリキー順にデータが物理的に並んでいます。リーフノードに実際の行データが格納されているため、プライマリキーでの検索は非常に高速です。

-- プライマリキー検索は最速
SELECT * FROM users WHERE id = 42;

セカンダリインデックス

プライマリキー以外のカラムに貼るインデックスです。リーフノードにはプライマリキーの値が格納されています。

-- email にセカンダリインデックスがある場合
SELECT * FROM users WHERE email = 'alice@example.com';

この検索は2段階になります:

  1. email インデックスから id = 42 を取得
  2. クラスタインデックスから id = 42 の行データを取得(ブックマークルックアップ

等値検索・範囲検索・ソートへの効果

インデックスを使った検索の流れ セカンダリインデックス検索 WHERE email = 'alice@example.com' ① email インデックス alice → id = 42 id = 42 を取得 ② クラスタインデックス id = 42 → 行データ 行データを返す セカンダリ → PKの2段階アクセス カバリングインデックス SELECT name, email FROM users WHERE name = 'Alice' ① (name, email) インデックス Alice → email が既に入ってる! EXPLAIN: Extra = "Using index" 元テーブルへの アクセス不要! インデックスだけで結果を返す 最速パターン インデックスのリーフノードだけ読めばよく テーブルへのランダムアクセスが発生しない
図2: インデックスを使った検索の流れ

等値検索

SELECT * FROM products WHERE sku = 'ABC-001';

B-treeをルートから辿り、該当のリーフノードを O(log n) で特定します。

範囲検索

SELECT * FROM orders WHERE created_at BETWEEN '2026-01-01' AND '2026-03-31';

開始値をB-treeで特定したあと、リーフノードのリンクを辿って終了値まで順に読みます。インデックスなしだとテーブル全体を舐める必要があります。

ソート

SELECT * FROM users ORDER BY last_name;

last_name にインデックスがあると、リーフノードはすでにソート済みです。EXPLAINExtraUsing filesort が出ていたら、インデックスでソートが完結していない証拠です。

カバリングインデックス

カバリングインデックス? クエリで必要な全カラムがインデックスに含まれていて、テーブル本体を参照せずに結果を返せる状態。EXPLAINのExtraに`Using index`と表示される。非常に高速。 とは、クエリが必要とするすべてのカラムがインデックスに含まれている状態です。セカンダリインデックスの場合、通常はインデックスでプライマリキーを見つけてから元テーブルにアクセスしますが、必要なカラムがすべてインデックスに入っていれば元テーブルへのアクセスが不要になります。

-- name と email だけ取得する場合
SELECT name, email FROM users WHERE name = 'Alice';
-- name と email を含む複合インデックスを作成
ALTER TABLE users ADD INDEX idx_name_email (name, email);

EXPLAINExtraUsing index と表示されればカバリングインデックスが効いています。これは最も高速な状態です。

いつインデックスを作るべきか

インデックスはタダではありません。以下のコストがあります:

  • ディスク容量:インデックスの分だけ容量が増える
  • INSERT/UPDATE/DELETE の遅延:データ変更時にインデックスも更新が必要
  • メモリ:InnoDB のバッファープールでインデックスをキャッシュするメモリが必要

インデックスを作るべきケース

  • WHEREJOIN ONORDER BYGROUP BY で頻繁に使われるカラム
  • カーディナリティ(値の種類数)が高いカラム(ユーザーID、メールアドレスなど)
  • 外部キー

インデックスを作らなくていいケース

  • カーディナリティが低いカラム(ステータスが3種類しかないなど)
  • ほとんどアクセスされないカラム
  • 小さなテーブル(数千行以下ならフルスキャンも十分速い)

次のエピソードでは、インデックスを貼ったのになぜか使われない——そういうケースを体系的に学びます。