SQL Scale Lab
第一部 DB 基礎編 · 第 8 章 · 約 25 分

インデックスの正体

SHOW INDEX でインデックスの構造を直接確認し、EXPLAIN を使って「全件スキャン」と「インデックスによる高速検索」の違いを実感します。

この章で答える問い
  1. 索引(インデックス)とは物理的にどのようなものなのか
  2. DB が「全件スキャンした」のか「インデックスで検索した」のかはどこでわかるか
  3. インデックスはいつ作られ、どのようなトレードオフを伴うのか

インデックスの構造 1: 名前と対象列の定義

orders にある索引
期待: 2 行。PRIMARY と customer_id

2 行の結果が返ってきます。Key_name がインデックス名、Column_name が対象の列名を表しています。 orders テーブルには「PRIMARY(主キー id のインデックス)」と「customer_id のインデックス」がすでに存在しています。 主キーを設定するとインデックスは自動的に生成されます。また customer_id のインデックスは、第 4 章で外部キー制約を定義した際に MySQL が自動生成したものです。 テーブル一覧パネルで ▪ マークが付いている列が該当し、インデックスとは「テーブル本体とは別に存在する、名前と対象列を持つ独立したデータ構造」であることが確認できます。

インデックスの構造 2: ストレージ上のデータサイズ

テーブル本体と索引の大きさ
期待: orders の index_length に数字がある

data_length はテーブル本体の行データの総容量、index_length はインデックス全体の容量(バイト単位)を示しています。 インデックスは論理的な概念にとどまらず、ディスク上に独立したバイナリデータとして物理的に格納されています。 この章の後半で新しいインデックスを作成した際、この数値が実際に増加する様子を確認してみましょう。

インデックスの構造 3: 実行計画で見る探索の足跡

この章から、実行ラボに「推定プラン」「実測プラン」というタブが追加されます。 これはデータベースが「目的の行をどのように探索したか」を記録した実行計画であり、第二部(性能編)で徹底的に読み解いていく最重要ツールです。 現時点では、実測プランの最下行に注目してください。そこが Table scan(全件走査)になっているか、 それとも Index lookup(インデックスによるピンポイント探索)になっているかを確認します。

索引のない列で探す
期待: 実測プランの一番下: Table scan on orders。読んだ行数 50
主キーで探す
期待: Single-row index lookup。読んだ行数 1
customer_id で探す
期待: Index lookup on orders using customer_id。読んだ行数 8

同じ SELECT 文であっても、データベースの探索アプローチは全く異なります。status 列にはインデックスが存在しないため、全 50 行を先頭から最後まで愚直に走査(Table scan)し、一致する行を拾い出しています。 一方、id や customer_id にはインデックスが存在するため、該当する行だけを直接読み取っています。 計器パネルに表示される「読んだ行数」の差が、探索効率の違いを如実に物語っています。

インデックスとは「本の巻末索引」のようなもの

Table scan: 行を先頭から全部読む(WHERE status = 'paid')…id 1(paid)= 残すid 5050 行全部読んで、条件に合う 1 行だけ残す。読む時間は行数に比例する。Index lookup: 索引で id を引き、その行だけ読む(WHERE customer_id = 3)customer_id → id2 → 25, 26, 35, 463 → 4, 6, 8, 11, 19, 20, 24, 474681119202447読んだ行数 8。他の 42 行には触らない。
Table scan は最初から全行を読み進めます。Index lookup はインデックスで目的の行の位置(id)を引き、その行だけを読み取ります。

テーブルのレコードは基本的に主キー(id)の順にストレージへ格納されています。そのため id での検索は瞬時に行えます。 しかし、データは status 順や ordered_at 順に並んでいるわけではないため、インデックスがなければ全件を端からチェックするしかありません。 インデックスとは、「ordered_at の値 → その値を持つ行の主キー」のペアをあらかじめ ordered_at 順に整列して保持しておく別の構造であり、 書籍の「巻末索引」とまったく同じ役割を果たしています。索引を引けば、目的のデータがどこにあるかが即座に特定できます。

ここでは概念的な理解にとどめ、インデックスのより深い内部構造(ページ、ノード、B-tree)については第二部 1 章で詳しく解き明かします。 50 件程度の少量データでは探索速度に体感上の差は現れませんが、第二部 1 章では 3,200 万行の巨大データを用いて、4 秒かかる全件走査がインデックスによって 0.01 秒未満に短縮される圧倒的な差を体感します。

インデックスを作成してみる

ordered_at に索引を作る
期待: エラーなく終わる
ordered_at で探す(索引あり)
期待: Index lookup (idx_ordered)。読んだ行数 1
大きさをもう一度
期待: index_length が増えている

構文は CREATE INDEX インデックス名 ON テーブル名 (対象列) です。 インデックスを作成すると、実測プランの探索方法が Table scan から Index lookup へと切り替わり、ディスク上のストレージ容量(index_length)が増加していることが確認できます。

インデックスの代償(トレードオフ)

インデックスは検索を劇的に高速化しますが、タダではありません。 テーブルに新しい行を追加・更新するたびに、データベースはすべてのインデックス構造も最新の状態に同期更新しなければなりません。 もし 5 本のインデックスが張られていれば、1 行の INSERT ごとに本体+5 本の索引という計 6 箇所への書き込みが発生し、ディスク容量やメモリ(バッファプール)も消費します。 そのため、むやみに全列へインデックスを張るのではなく、「頻繁に WHERE 句や JOIN 結合、ORDER BY に利用される列」を厳選して設計する必要があります。

後始末
期待: エラーなく終わる

インデックスはテーブル本体とは別にディスク上に構築される検索用の索引です。実行計画を確認することで、全件走査なのかインデックス検索なのかを正確に判別できます。

確認

あるクエリが全部読んだのか索引で引いたのかは、どこを見ればわかるか。
実測プランの一番下の行を見る。Table scan なら行を先頭から全部読んでおり、Index lookup なら索引で引いて該当する行だけを読んでいる。計器の「読んだ行数」も同じことを表す。
索引を作ると増えるものは何か。それはどこで確かめられるか。
索引はテーブル本体とは別のバイト列としてディスク上に作られるので、information_schema で見た index_length が増える。索引はただの得ではなく、場所と、行を足すたびに直す手間を代償に払っている。
開発環境で速かったクエリが本番で遅い、を動作確認で見つけられないのはなぜか。
行が少なければどう探しても一瞬で、探し方の差が時間に現れないから。だから WHERE / JOIN / ORDER BY に使う列に索引があるかは、動かして確かめるのではなくコードレビューで見る。

次の章へ

ここまでの SQL はすべてブラウザ上のラボから直接発行してきました。 しかし実務の本番システムでは、バックエンドのアプリケーションコード(Node.js, Python, Go, Java 等)や ORM を介して SQL が発行されます。 次の章では、アプリケーションからデータベースを操作する際に直面する「SQL インジェクション」「ORM の隠蔽」「N+1 問題」という 3 大トラブルを取り上げ、第一部の総仕上げを行います。