データベースのインデックスは検索を高速化する強力な仕組みですが、内部構造と設計指針を理解せずに作成すると、効かないインデックスや書き込みを遅くするだけのインデックスを量産してしまいます。本記事ではインデックスの内部構造、作成すべき/すべきでないカラムの判断基準、複合インデックスの設計、効かないSQLパターン、カバリングインデックス、各RDBMSのインデックス種類、書き込みへの影響、アンチパターン、チューニングワークフローまでを横断的に整理します。
主に B-tree インデックスを前提にしますが、PostgreSQL の GiST/GIN/BRIN や MySQL の不可視インデックスなど RDBMS 固有機能にも触れます。
インデックスとは何か
インデックスはテーブルとは別のオブジェクトとして保存される、純粋に冗長なデータ構造です。書籍の巻末索引と同じく、本体(テーブル)を変更せずに、特定の値を高速に発見するための参照リストを別に持ちます。データベースは insert / update / delete のたびにインデックスを自動メンテナンスし、テーブルとの一貫性を保ちます。
インデックスの目的は「インデックスを張った列に対して順序付けされたアクセスを提供すること」。ところが INSERT のたびに物理的にデータを並べ替えていたら大量のデータ移動が発生してしまいます。これを避けるため、データベースは物理的な格納順とは独立した論理的な順序を作ります。論理順序を維持するために使われるのが 双方向連結リスト と B-tree という2つのデータ構造で、ほとんどの RDBMS のパフォーマンス特性はこの2つの組み合わせで説明できます。
B-tree の内部構造
B-tree インデックスは 双方向連結リスト + balanced tree という2つのデータ構造で実装され、論理順序と物理位置を分離しています。インデックス検索は「ツリー走査 → リーフノードチェーン走査 → テーブルアクセス」の3ステップで行われ、対数的スケーラビリティ(数百万行でも木の深さ4〜5)を持つのが強みです。
「インデックスを使っているのに遅い」の正体は (1) リーフノードチェーン走査範囲が広い/(2) テーブルアクセスが多い で、この組み合わせはフルスキャンより遅くなることもあります。
リーフノード/ブランチノード/ルートノードの構造図、3ステップの詳細(INDEX UNIQUE SCAN / INDEX RANGE SCAN / TABLE ACCESS BY INDEX ROWID)、各 RDBMS のオペレーション名対応、「遅いインデックス」の本当の原因については以下を参照してください。
インデックスを作成すべきかの判断基準
インデックスは無料ではありません。「テーブル規模・絞り込み率・更新頻度・利用箇所(WHERE/ORDER BY/GROUP BY/JOIN)」の4観点でチェックします。具体的なチェックリスト・カーディナリティと選択性の違い・データ分布が時間とともに変化するリスク・主キー/ユニーク/外部キーの自動インデックス化の扱いは 基礎から学ぶインデックス作成時のチェックポイント を参照してください。
ここでは本記事固有の論点のみ補足します。
「選択性(selectivity)」の定義は DBMS によって違う
「選択性」という用語は DBMS によって計算式が違うので注意が必要です。
- MySQL:
COUNT(DISTINCT col) / COUNT(col)— 値の種類数の割合(カーディナリティ寄り)。MySQLでは「選択性が低いとインデックスの有効性も低下」 - Oracle / SQL Server / DB2 / PostgreSQL 等: 「テーブルの行の総数に対し、抽出条件を適用した結果の行数が占める割合」(絞り込み率)。こちらの定義では「選択性が高いとインデックスの有効性が低下」と意味が逆
書籍やブログで「選択性が高い/低い」と書いてあるとき、どの定義かを確認するのが大事です(SQLアンチパターン 12章 監訳注)。
インデックス作成は開発者の仕事
「どんな WHERE 句がどの頻度で発行されるか」「ある列の選択性が業務的にどの程度か」というアクセスパスの情報を持っているのは、コードを書いている開発者だけです。DBA や外部コンサルタントが事後にリバースエンジニアリングで導き出すのは時間がかかりすぎ、しかも 1 つのクエリだけ最適化して他を悪化させる罠もあります。インデックス設計は DDL レビュー込みで開発者の責務として引き受けるのが最善です。
複合インデックスの設計
複数のカラムにまたがる1つのインデックスを 複合インデックス(連結インデックス、マルチカラムインデックス)と呼びます。(A, B, C) という複合インデックスはまず A で並べ、A の同値内で B、B の同値内で C で並べる電話帳と同じ規則で構築されます。
要点は以下の3つです。
- 最初の列から連続して指定された範囲までしかアクセス述語にならない(電話帳で姓を知らずに名前だけで検索できないのと同じ)
- 列順は 「最も選択性の高い列を先頭」ではなく「アプリケーションの WHERE 句に出る組み合わせを最大限カバー」 で決める
- 等価条件を先、範囲条件を後 に置く(範囲条件以降の列はインデックスとして機能しなくなる)
- ソート向き混在(
ASC, DESC)対応は MySQL 8.0+ / MariaDB 10.8+ の降順インデックスが必要
検索条件とインデックス利用可否の対応表、検索+ソートの組み合わせパターン、降順インデックスの構文などの詳細は 【図解】B-treeを理解し、複合インデックスの順番を正しく作る を参照してください。
アクセス述語とフィルタ述語
複合インデックス設計の核心は「アクセス述語(B-tree走査範囲を絞り込む条件)」と「フィルタ述語(走査後に各行で評価する条件)」の見分けにあります。実行計画上は両方とも「インデックスを使っている」ように見えますが、フィルタ述語はデータ量が増えると比例的に応答時間が伸びるため、Markus Winand は 「不発弾のようなもの」 と表現しています。
各 RDBMS での確認方法:
| RDBMS | アクセス述語 | フィルタ述語 |
|---|---|---|
| Oracle | Predicate Information の access | Predicate Information の filter |
| MySQL 8.0+ | EXPLAIN FORMAT=TREE の Index lookup on ... | Filter: |
| PostgreSQL | Index Cond | Filter |
| SQL Server | Seek Predicates | Predicate |
| Db2 | START/STOP | SARG |
述語の判別が複合インデックスの列順設計にどう跳ね返るか、具体的なクエリパターンや EXPLAIN 出力例は以下を参照してください。
インデックスが効かない/効きにくい SQL
一般的に効かないと言われるケース
書籍などで「インデックスが効かない」と言われる典型パターン:
- インデックス列に対して演算をする
- インデックス列に対して SQL 関数を適用している
- 否定条件(
<>、!=、NOT IN)を利用している - 後方一致、もしくは中間一致で LIKE を利用している
- 暗黙の型変換を利用している
IS NULLを利用している- OR を利用している
- スマートロジック(
OR :var IS NULL) - 列の連結
- 数式 WHERE
検証で確認できること
実環境(MySQL 5.7+ / 8.0+)での検証によると、上記のうち 暗黙の型変換・IS NULL・OR は実際にはインデックスが利用されるケースが多い です。MySQL のオプティマイザはこれらをサポートしています。一方、演算・関数適用・否定条件・後方/中間 LIKE は確実に効かないのでこれらは避ける必要があります。
効かないケースの本質
「列を関数や式で包んだ瞬間、データベースから見るとそれは元の列とは無関係のブラックボックスになる」というのが本質です。UPPER(last_name)、TRUNC(sale_date)、numeric_string + 0、(date_string || time_string) のように列を変換すると、通常のインデックスは効きません。
インデックスを利用するときは、条件式の左辺は裸
これがインデックスまわりのチューニングの基本の「き」です(達人に学ぶSQL徹底指南書 1-11)。式や関数の適用は右辺で行い、左辺のテーブル列はそのままにする。
例:
-- インデックスが効かない(演算)
SELECT * FROM users WHERE age / 2 = 15;
-- インデックスが効く(右辺で演算)
SELECT * FROM users WHERE age = 30;
SELECT * FROM users WHERE age = 60 / 2;
-- インデックスが効かない(関数適用)
SELECT * FROM users WHERE UPPER(last_name) = 'WINAND';
-- インデックスが効く(リテラル側を変換)
SELECT * FROM users WHERE last_name = 'WINAND';
-- 後方/中間一致(インデックス効かず)
WHERE last_name LIKE '%A%'
WHERE last_name LIKE '%A'
-- 前方一致(インデックス効く)
WHERE last_name LIKE 'A%'
中間一致 LIKE をどうしても使いたい場合は、全文検索インデックスを導入します。
| RDBMS | ツール | URL |
|---|---|---|
| MySQL | Mroonga | http://mroonga.org/ja/ |
| PostgreSQL | PGroonga | https://pgroonga.github.io/ja/ |
| PostgreSQL | pg_bigm | https://pgbigm.osdn.jp/ |
| PostgreSQL | pg_trgm | (PostgreSQL 公式拡張、3-gram で中間一致を高速化) |
解決策
(a) 関数インデックス/式インデックス/生成列インデックスを作って、インデックスの定義式と SQL 中の式を完全一致させる。
-- Oracle / PostgreSQL
CREATE INDEX emp_up_name ON employees (UPPER(last_name));
-- MySQL 8.0+ の関数インデックス
CREATE INDEX emp_up_name ON employees ((UPPER(last_name)));
-- MySQL 5.7 / 8.0 の生成列+インデックス
ALTER TABLE employees ADD COLUMN last_name_upper VARCHAR(255) GENERATED ALWAYS AS (UPPER(last_name));
CREATE INDEX emp_up_name ON employees (last_name_upper);
(b) 検索語側を変換して、テーブル列はそのままにする(こちらが優先)。
-- 良くない: 列を変換
WHERE TO_CHAR(sale_date, 'YYYY-MM-DD') = '2024-01-01'
-- 良い: 検索語側を変換、テーブル列はそのまま
WHERE sale_date = TO_DATE('2024-01-01', 'YYYY-MM-DD')
-- もっと良い: 範囲条件に書き換え
WHERE sale_date >= DATE '2024-01-01'
AND sale_date < DATE '2024-01-02'
(c) 動的 SQL で必要な条件だけを含む WHERE 句を構築する(スマートロジックの代わり)。
スマートロジックの罠
「1 つのクエリで全パターンの検索条件をカバーする」つもりで、以下のように OR で条件をキャンセル可能にする書き方を「スマートロジック」と呼びます:
SELECT * FROM employees
WHERE ( subsidiary_id = :sub OR :sub IS NULL )
AND ( employee_id = :emp OR :emp IS NULL )
論理的には正しいのですが、データベースは実行時にどの条件が有効かわからないため、全条件が無効になる最悪ケースに備えてフルテーブルスキャンを選んでしまい、各列にインデックスがあっても全く使われません。
代わりに動的 SQL でその時必要な条件だけを含む WHERE 句を構築します(バインドパラメータは引き続き使用)。
カバリングインデックス(インデックスのみのスキャン)
SELECT 句で参照される全ての列がインデックスに含まれていれば、テーブル本体へのアクセスを完全に省略できます。これを インデックスのみのスキャン/カバリングインデックス と呼び、Markus Winand は「あらゆるチューニング法の中でも最強の方法のひとつ」と評価しています。
ただし WHERE 列を 1 つ足すだけでカバリングが崩れて遅くなる「不合理な変化」、SQL Server / PostgreSQL 11+ / Db2 の INCLUDE 句、欲張って列を増やすアンチパターンといった注意点があります。
仕組み・各 RDBMS での確認方法(Using index / Index Only Scan)・崩れる典型例・INCLUDE 句の使いどころ・過剰設計を避ける指針については以下を参照してください。
ソートを発生させる演算とインデックス
SQL では明示的にソートを書かなくても、内部的にソートが発生する演算が多数あります。物理メモリを使ったソートは論理メモリの大幅に遅くなる(巷では100万倍と言われる)ので、無駄なソートは可能な限り回避するのがチューニングの基本です。
ソートが発生する代表的な演算(達人に学ぶSQL徹底指南書 1-11):
- GROUP BY 句
- ORDER BY 句
- 集約関数(SUM、COUNT、AVG、MAX、MIN)
- DISTINCT
- 集合演算子(UNION、INTERSECT、EXCEPT)
- OLAP関数(RANK、ROW_NUMBER 等)
インデックスでソートを軽減・スキップする手段:
- ORDER BY / GROUP BY をインデックス順序と一致させる: B-tree のリーフは元々ソート済みなので、インデックスでスキャンすればソート処理(filesort)をスキップできる
- MAX / MIN 関数: インデックスがあればインデックスのスキャンだけで済み、テーブル本体への検索を回避できる
- UNION ALL を使う: UNION は重複排除のためソートするが、UNION ALL はソート不要
- DISTINCT を EXISTS で代用: 結合結果を一意化する目的の DISTINCT は EXISTS に書き換えるとソートを回避できる
- WHERE で書ける条件は HAVING に書かない: HAVING は集約後にフィルタするのでインデックスを引き継げない場合が多い。事前に WHERE で行を絞ったほうが効率的
-- 遅い: HAVINGで絞る
SELECT sale_date, SUM(quantity) FROM sales
GROUP BY sale_date HAVING sale_date = '2024-01-01';
-- 速い: WHEREで絞ってから集約
SELECT sale_date, SUM(quantity) FROM sales
WHERE sale_date = '2024-01-01' GROUP BY sale_date;
SQL の書き方とインデックスの効率
同じ結果を返す SQL でも、書き方によってインデックスの利用効率が変わります(達人に学ぶSQL徹底指南書 1-11)。
サブクエリは IN より EXISTS / 結合に書き換える
-- 遅い: サブクエリ+IN(ワークテーブルが作られる)
SELECT * FROM Class_A
WHERE id IN (SELECT id FROM Class_B);
-- 速い: EXISTS(結合キーのインデックスだけ参照、1行一致で打ち切り)
SELECT * FROM Class_A A
WHERE EXISTS (SELECT * FROM Class_B B WHERE A.id = B.id);
-- 速い: 結合(INNER JOIN)に書き換え
SELECT A.id, A.name FROM Class_A A
INNER JOIN Class_B B ON A.id = B.id;
EXISTS が速い理由は (1) 結合キーにインデックスがあれば実表を見ずインデックス参照だけで済む、(2) 一行でも条件合致したら検索を打ち切る、の2点です。
中間テーブルを減らす
サブクエリの結果を中間テーブルとして展開すると、メモリ消費+元テーブルのインデックスが使えなくなります。可能な限り中間テーブルを省くことがパフォーマンス向上の鍵です。
統計情報とオプティマイザ
オプティマイザはどのインデックスを使うか、どのアクセスパスが最短かを 統計情報 を頼りに判断します。統計情報が古いと「地図が古くなったカーナビ」と同じで実行計画が劣化します。
統計情報の基本的な役割(行数・分布・カーディナリティ・NULL率)と更新コマンド(ANALYZE TABLE / ANALYZE)、「インデックスがあるはずなのに使われない」ケースの典型原因については 基礎から学ぶインデックス作成時のチェックポイント を参照してください。
ここでは運用上の応用論点のみ補足します。
統計情報収集のタイミング
データが大きく更新された後、なるべく早く
INSERT/UPDATE/DELETE のいずれもデータの値の分布や偏りを変えうるので、大量更新後は統計情報を更新する。ただし統計情報収集自体が重い処理(場合によっては数時間)なので、基本は夜間帯に実施します。
DBMS によってはデフォルトで自動収集が走ります(Oracle 11g は夜22時など)。マニュアルで現状の設定を確認するのが推奨です。
統計情報の凍結
逆に、統計情報を意図的に凍結して、現在の実行計画を将来も維持したい場合があります。データ量が増えると実行計画が変わってパフォーマンスが劣化することを避けたい場合の悲観的設計です。サービス終了時のデータ量を想定したテストデータで統計を作ってから凍結する、という運用です。ただしサービス終了時を想定したテストデータの準備が現実には極めて大変なので、データ量の変化が小さいシステムでしか実用的ではありません。
統計情報が原因の「再現しない実行計画」
スロークエリの原因が「本番では遅いがステージングでは速い」という場合、統計情報の差が原因のことがあります。本番でバッチ処理直後のサンプリングで偏った統計が作られるなど。インデックスや統計情報を固定するヒントを使う手もあります(飛び道具)。
| RDBMS | ヒント・固定方法 |
|---|---|
| MySQL | インデックスヒントの構文(USE INDEX / FORCE INDEX 等) |
| PostgreSQL | pg_hint_plan 拡張、pg_dbms_stats(統計情報管理) |
インデックスの種類
B-tree がほぼ常に第一選択ですが、以下のような用途では専用インデックスが有効です。
- 等価のみ/範囲不可: ハッシュインデックス(PostgreSQL の
USING HASH、MySQL InnoDB の適応的ハッシュ) - 中間一致 LIKE / 全文検索: FULLTEXT / GIN+tsvector / Mroonga / pg_bigm / pg_trgm
- 配列 / JSON / 地理空間 / 範囲型(PostgreSQL): GiST / GIN / SP-GiST
- 時系列の超巨大テーブル(PostgreSQL): BRIN(軽量・物理順と相関する列に有効)
- 関数や式の結果で検索: 関数インデックス(MySQL 8.0+ / Oracle / PostgreSQL)
- 特定条件の行だけインデックス化: 部分インデックス(PostgreSQL / SQL Server。MySQL 非対応)
- テーブル本体を持たない構造: クラスタ化インデックス(MySQL InnoDB の主キー、SQL Server、Oracle IOT)
- DWH の多列 AND/OR: ビットマップインデックス(Oracle / Db2。OLTP では書き込み性能が破綻するので不可)
各インデックスの仕組み・対応 RDBMS の一覧表・選び方のフローチャート・実務上の注意点は以下を参照してください。
インデックスのコスト(書き込みへの影響)
インデックスは冗長データなので、書き込みで一貫性を保つコストがかかります。INSERT は WHERE がないため利益ゼロで、インデックス 1 個追加するだけで挿入コストが劇的に変わります。DELETE はインデックス数に比例して遅くなり、UPDATE は対象列を含むインデックスのみ更新されます。
INSERT のスケール感(インデックス 1 個で約100倍)、バルクロード時にインデックスを落とす運用、TRUNCATE TABLE と DELETE の使い分け、PostgreSQL MVCC の DELETE 特性、ORM の「全列 UPDATE」問題(Hibernate / Active Record / Doctrine / TypeORM の違い)については以下を参照してください。
アンチパターン
『SQLアンチパターン』12章「インデックスショットガン」では、推測に基づいてインデックスを使うことで起きる3つのミスを指摘しています。
- インデックスをまったく定義しないか、少ししか定義しない(書き込みオーバーヘッドへの過剰反応)
- インデックスを多く定義し過ぎる/役立たないインデックスを定義する(過剰インデックス、未使用放置、関数インデックス重複)
- インデックスを活用しないクエリを実行する(左辺に関数・演算、中間/後方一致 LIKE、否定条件など)
加えて、ORM の「全列 UPDATE」問題、ANSI SQL がインデックスを規定していないこと、そして 「インデックスは劣化する/選択性の高い列を最初に/NULL は不可/動的 SQL は遅い」 といった都市伝説に振り回されないことが重要です。
各アンチパターンの詳細・未使用インデックスの検出 SQL(MySQL sys.schema_unused_indexes / PostgreSQL pg_stat_user_indexes)・「左辺は裸」原則・都市伝説の正体については以下を参照してください。
なお、削除候補のインデックスを「不可視化して観察→ DROP」で安全に検証する手順は別記事にまとめています。
パフォーマンスチューニングのワークフロー
インデックスチューニングは「重いクエリ特定 → EXPLAIN → 述語確認 → インデックス修正 → 再EXPLAIN」の繰り返しで進めます。各ステップの具体的なコマンド・EXPLAIN 出力例・MENTOR 原則の枠組みは以下の記事に分散しています。
ここでは本記事固有の論点のみ補足します。
データベースがボトルネックとは限らない
「アプリが遅い → データベースが原因」と決めつけるのは早計です。プロファイリングツールを使ってアプリ側のコードも測定し、本当にDBがボトルネックかを確認する。HTML パース処理がコストの 80% を占めていた、という話もあります。仮説を立てる前にソフトウェア診断ツールを実行するのが鉄則です(SQLアンチパターン 12章)。
本番中に CREATE INDEX するときの注意
PostgreSQL では本番中に作る場合 CREATE INDEX CONCURRENTLY を使うとロックを取らずに作成できます。MySQL の InnoDB はオンラインDDLが標準で、ALGORITHM=INPLACE, LOCK=NONE でロック影響を最小化できますが、テーブルサイズが極端に大きい場合は pt-online-schema-change / gh-ost のような外部ツールも選択肢です。
実データに近い環境で計測する
同じテーブルでもレコード数やデータの分散によって実行計画は変わります。本番に数十万件あるなら、チューニングも同等のデータで行うこと。Faker 等でダミーデータを大量投入して検証します。
インデックスのメンテナンス(再構築)
長期にわたって行の更新・削除を繰り返すと、インデックスは徐々に不均衡になっていきます。完全に劣化するわけではないですが、断片化率が上がるとパフォーマンスに影響します。定期的なメンテナンスを推奨。
各 DBMS のメンテナンスコマンド(『SQLアンチパターン』12章):
| DBMS | コマンド |
|---|---|
| IBM DB2 | REBUILD INDEX |
| Microsoft SQL Server | ALTER INDEX ... REORGANIZE, ALTER INDEX ... REBUILD, DBCC DBREINDEX |
| MySQL | ANALYZE TABLE, OPTIMIZE TABLE |
| Oracle | ALTER INDEX ... REBUILD |
| PostgreSQL | VACUUM, ANALYZE, REINDEX |
| SQLite | VACUUM |
注: Use The Index, Luke! の著者は「インデックスは劣化する」を都市伝説と位置づけ、再構築の効果は限定的(INDEX UNIQUE SCAN レベルでは 0〜2% 改善)と主張しています。再構築の頻度は使用頻度・更新頻度・テーブルサイズで判断するのが現実的で、「週1回」のような一般則はあてになりません。
RDBMS 別の主要な差異
MySQL 8.0 の特徴
- InnoDB クラスタ化インデックス: 主キーがクラスタ化インデックスとして強制
- 降順インデックス: 8.0 から本物の降順インデックス(それ以前は逆方向スキャンで対応していたためパフォーマンスが落ちた)
- 不可視インデックス:
INVISIBLEで削除前のテストが可能 - 関数インデックス: 8.0 から正式サポート(
CREATE INDEX ... ON tbl ((UPPER(col))))。それ以前は生成列+インデックスで代用 - NULLS FIRST/LAST はインデックス定義で未サポート
PostgreSQL の特徴
- 多様なインデックス種類: B-tree / Hash / GiST / SP-GiST / GIN / BRIN / Bloom
- 部分インデックス:
CREATE INDEX ... WHERE ...で特定行のみインデックス化 - INCLUDE 句: 11+ で非キー列を保存できる
- CREATE INDEX CONCURRENTLY: ロックを取らずにインデックス作成
- NULLS FIRST/LAST: インデックス定義・ORDER BY 両方で完全サポート
- MVCC: DELETE は削除済みフラグだけで、物理削除は VACUUM
- pg_trgm 拡張: 中間一致 LIKE もインデックス化可能
Oracle の特徴
- NULL の扱い: 全列 NULL の行はインデックスに含まれない(ただし2列目に定数式で回避可能)
- IOT(索引構成表):
ORGANIZATION INDEXで作成 - INDEX SKIP SCAN: 複合インデックスの先頭列が指定されていなくても、先頭列のカーディナリティが低ければ使われる場合がある
- 関数インデックス: 古くからサポート
SQL Server の特徴
- クラスタ化インデックス: 主キーがデフォルトでクラスタ化(
NONCLUSTEREDで外せる) - INCLUDE 句: カバリング用の非キー列をサポート
- フィルタ選択されたインデックス: PostgreSQL の部分インデックスに相当
- Index Seek + RID Lookup / Key Lookup: 実行計画の用語
チェックリスト: インデックスを作る前に確認すること
- テーブルのレコード数は十分大きいか(目安: 1万件以上)
- そのカラムで全体の 5〜15% まで絞り込めるか
- そのカラムは更新頻度が低いか(高ければトレードオフ判断)
- WHERE / ORDER BY / GROUP BY / JOIN / 集計 のいずれかで使われるか
- 主キー・ユニークキー(自動でインデックスされる)ではないか
- 同じカラムに既に類似のインデックスがないか
- 複合インデックスにできないか(既存インデックスに列を追加する形で代用できないか)
- 列順は「アプリケーションの WHERE 句」を最大限カバーするか
- 等価条件の列が先頭、範囲条件の列が後ろになっているか
- WHERE 句で関数や演算で列を包んでいないか
- スマートロジック(
OR :var IS NULL)になっていないか
チェックリスト: 既存インデックスを見直すとき
- EXPLAIN で実行計画を取得し、Predicate Information を確認したか
- アクセス述語とフィルタ述語の区別がついているか
- 未使用インデックスがないか確認したか(
sys.schema_unused_indexes/pg_stat_user_indexes) - 重複・冗長なインデックスがないか
- 削除候補は不可視化(MySQL 8.0+)で本番テストしたか
- 統計情報は最新か(オプティマイザの判断材料)
- 本番相当のデータ量で検証したか
まとめ
- インデックスは双方向連結リスト+B-tree という 2 つのデータ構造で実装され、論理順序と物理位置を分離している
- 検索は「ツリー走査 → リーフノードチェーン走査 → テーブルアクセス」の 3 ステップ。遅さの原因は後ろの 2 つ
- インデックス作成は開発者の仕事。WHERE 句の組み合わせを知っているのは開発者だけ
- 複合インデックスの列順は「アプリケーションの WHERE 句を最大限カバー」が正解。「最も選択性の高い列を最初に」は迷信
- アクセス述語とフィルタ述語の区別が複合インデックス設計の核心。フィルタ述語は不発弾
- 列を関数や式で包むとインデックスは効かない。検索語側を変換するか、関数インデックスを作る
- カバリングインデックスは強力だが、欲張って列を増やすのは避ける。WHERE を優先
- インデックスは無料ではない。INSERT は乗数的に遅くなる。未使用インデックスは検出して削除
- ORM の「全列 UPDATE」「N+1 問題」「全列 SELECT」は典型的なアンチパターン
- 都市伝説(劣化する/選択性の高い列を最初に/NULL は不可)に振り回されない
- チューニングは「重いクエリ特定 → EXPLAIN → 述語確認 → インデックス修正 → 再 EXPLAIN」の繰り返し
参考資料
書籍
- Use The Index, Luke! (SQLパフォーマンス詳解) - Markus Winand
- 第1章 インデックスの内部構造(リーフノード、B-tree、遅いインデックス)
- 第2章 where 句(複合インデックス・関数・処理しにくい条件)
- 第5章 データのクラスタリング(カバリングインデックス)
- 第8章 挿入・削除・更新(DML とインデックスのコスト)
- 達人に学ぶDB設計 徹底指南書
- 第6章 データベースとパフォーマンス(B-treeインデックスの設計方針、統計情報)
- 達人に学ぶSQL徹底指南書
- 1-11 SQLを速くするぞ 〜お手軽パフォーマンス・チューニング(左辺は裸、IN/EXISTS、ソート回避)
- 失敗から学ぶRDBの正しい歩き方
- 第4章 効かないINDEX(INDEX が効かない5パターン、データの比率変化)
- SQLアンチパターン
- 第12章 インデックスショットガン(3つのミス、MENTOR原則、カバリングインデックス)
公式ドキュメント
- MySQL 8.0 リファレンスマニュアル: インデックスの使用の仕組み
- MySQL 8.0 リファレンスマニュアル: 不可視のインデックス
- MySQL 8.0 リファレンスマニュアル: 降順インデックス
- MySQL 8.0 リファレンスマニュアル: schema_unused_indexes ビュー
- PostgreSQL ドキュメント: インデックスの種類