技術的定義と内部構造
InnoDBストレージエンジンでは、クラスタードインデックス(主キー索引)がテーブルの実データそのものを格納します。これに対しセカンダリインデックス(非主キー索引)には、索引対象の値と当該行の主キーのみが保存されます。
このアーキテクチャにより、セカンダリインデックスを経由して検索を実行した際、検索条件を満たす主キー値は取得できますが、他の列のデータは保持していません。したがって、WHERE句以外のカラムを参照する必要がある場合、DBエンジンは取得した主キー値を元にクラスタードインデックスを再度走査し、実際の行データを取得しなければなりません。この二段階の索引走査プロセスを一般的に「回表」(Table Lookup)と呼びます。
索引走査シナリオの比較
以下に、実際のクエリ実行時に回表が発生するケースと発生しないケースを、サンプルスキーマを用いて解説します。
CREATE TABLE products (
item_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
sku_code VARCHAR(20) UNIQUE,
stock_qty INT DEFAULT 0,
category_id INT,
PRIMARY KEY (item_id),
INDEX idx_category (category_id)
) ENGINE=InnoDB;
回表が発生しないケース
クラスタードインデックス利用時
主キーを直接指定する場合、インデックスリーフノードに完全な行データが含まれているため、追加の探索は不要です。
-- item_idはクラスタードインデックスの一部であるため、行データが直接取得可能
SELECT item_id, sku_code, stock_qty
FROM products
WHERE item_id = 50092;
カバリングインデックス適用時
セカンダリインデックスがクエリで要求されるすべてのカラムをカバーしている場合、エンジンはクラスタードインデックスを参照せずにインデックス情報だけで結果を返却します。
-- idx_category インデックスと SELECT句 の項目のみで完結するため回表なし
SELECT item_id, category_id
FROM products
WHERE category_id = 12;
回表が発生するケース
セカンダリインデックス利用かつ非索引カラム参照時
セカンダリインデックスで条件を絞った後、索引に含まれないカラム(例: sku_code, stock_qty)を取得しようとする場合、必ず回表が起きます。
-- idx_category で該当する item_id を特定した後、
-- 各行データへアクセスするためにクラスタードインデックスを再度走査する
SELECT item_id, sku_code, stock_qty
FROM products
WHERE category_id = 12;
この処理は論理的には「B+木の葉ノード→主キー解決→クラスタ木走査」という順序で実行され、ディスクI/Oやキャッシュミスが増加する要因となります。
パフォーマンスへの影響
回表処理は主に以下の点でクエリ性能に影響を与えます。
- ランダムI/Oの増大:クラスタインデックスの探索はインデックスブロック単位で行われるため、連続読み取りではなくランダムアクセスになりがちです。
- バッファプール効率の低下:インデックスブロックだけを読んだ場合と比較し、行データブロックまで読み込むことでメモリ使用パターンが断片化し、ヒット率が下がる可能性があります。
- 排他ロック範囲の拡大:トランザクション実行時に、インデックス行だけでなく実際の実データ行にもロックを付与する必要があり、同時実行数に対する競合リスクが高まります。
最適化アプローチ
高負荷環境や大型テーブルにおいて回表コストを抑えるためには、以下の設計方針を検討します。
- カバリングインデックスの構築:頻繁に実行されるクエリのSELECT・WHERE・JOIN条件を包含するように複合インデックスを設計する。
- 列明示によるデータ取得削減:
SELECT *を避け、必要な列のみを抽出することで、もしそれらがインデックスに含まれていれば回表を防げる可能性が高まる。 - フィルタリングの前置:WHERE句の選択性が高いカラムに索引を設定し、回表対象となる候補行数を極小化する。
- パーティショニングの併用:非常に広いスキャンが必要な場合に、テーブル分割により対象ブロックを局所化させる。