複合インデックスの列順設計 — 等値 → 範囲 → ソート
複合インデックスの列順は「等値 → 範囲 → ソート」。左端一致の再利用とよくある失敗を押さえる。
原則: 等値 → 範囲 → ソート
複合インデックスの列順は、次の 3 ステップの順に並べるのが基本原則です。この順序を外すとインデックスが使われない・使われても遅い状況に陥ります。
- 等値検索で使う列(
WHERE col = ?)を先頭に - 続けて範囲検索で使う列(
>=,BETWEEN,<,IN) - 最後に ORDER BY / GROUP BY で使う列
例: 「特定ユーザーの注文を新着順に 20 件取得」というクエリ
SELECT * FROM orders
WHERE user_id = 42
AND created_at >= '2026-01-01'
ORDER BY created_at DESC
LIMIT 20;
この場合の最適インデックスは (user_id, created_at)。user_id が等値で絞り込み、同じ user_id の中で created_at 降順がそのまま索引のソート順になるので、WHERE・ORDER BY・LIMIT が 1 本の索引で全部片付きます。
左端一致の再利用 — 1 本で複数クエリをカバーする
複合インデックス (a, b, c) は、左端から使う分には部分的に活用できます。つまり 1 本の索引で 3 種類のクエリをカバーできます。
WHERE a = ?→ 使えるWHERE a = ? AND b = ?→ 使えるWHERE a = ? AND b = ? AND c = ?→ 使えるWHERE b = ?→ 使えない(左端欠け)WHERE a = ? AND c = ?→aだけで絞ったあとcは範囲内走査
この性質から、列順を設計するときは「最もよく使う最短プレフィクス」から並べるのが有効です。たとえば SaaS でよくある設計パターン:
- ほぼ全クエリに
tenant_id = ?が付く →tenant_idを先頭 - その次によく付く絞り込みが
status/user_id→ 2 列目 - 新着順の並び替えは
created_at→ 3 列目
結果 (tenant_id, user_id, created_at) のような 1 本の索引が、テナント単位の絞り込み、ユーザー別一覧、新着順、それぞれのクエリを支えます。単列索引を 3 本貼るより索引本数が少なく、書き込みコストも抑えられます。
-- 1 本で複数クエリをカバーする
CREATE INDEX idx_orders ON orders (tenant_id, user_id, created_at);
-- ① テナント全体の新着
SELECT * FROM orders WHERE tenant_id = 1
ORDER BY created_at DESC LIMIT 20;
-- → (tenant_id) を使い、末尾 created_at でソート済み
-- ② テナント × ユーザー
SELECT * FROM orders WHERE tenant_id = 1 AND user_id = 42;
-- → (tenant_id, user_id) で絞り込み
-- ③ テナント × ユーザー × 新着順
SELECT * FROM orders WHERE tenant_id = 1 AND user_id = 42
ORDER BY created_at DESC LIMIT 20;
-- → 全列活用、索引だけで完結する可能性あり失敗 1: 範囲を先頭に置く
「created_at はよく使うから先頭にしよう」は典型的な失敗です。範囲条件を先頭に置くと、その後ろの列に対する絞り込みが索引上で効率よく走らない からです。
具体例:
-- ❌ 範囲先頭の索引
CREATE INDEX idx_bad ON orders (created_at, user_id);
SELECT * FROM orders
WHERE created_at >= '2026-04-01'
AND user_id = 42;
このクエリでは、created_at >= '2026-04-01' の範囲内の全エントリを索引から拾ってから user_id = 42 で絞り込むことになります。4 月以降に 1,000 万件の注文があれば、1,000 万エントリを舐めてから 42 番ユーザー分を抜き出す動作。
正解は列順を逆にして (user_id, created_at) にすること。等値で先にユーザーを絞り、そのユーザーの注文内で範囲検索が走ります。ユーザーあたり平均 100 件なら、索引走査量が 10 万分の 1 になります。
失敗 2: カーディナリティ順に並べる
古い教科書には「カーディナリティ(値の種類数)が高い列を先頭に」と書かれていますが、これはクエリパターンが不明な場合の一般論です。実際のクエリパターンが分かっているなら、クエリ側の優先順に合わせる方が常に正しい。
例: status(5 種類しかない)と user_id(10 万ユーザー)がある状況。
- クエリが
WHERE status = 'active' AND user_id = 42なら、user_id を先頭が速い(ユーザーで絞ると数百件に、その中で status で絞ると数件) - だが実際のクエリが
WHERE status = 'active' ORDER BY updated_atのようにuser_idを使わないなら、(status, updated_at)の方が索引を使える
「カーディナリティ高い列を先頭」に盲従すると、後者のクエリで索引が使われなくなります。クエリの WHERE 句を先に書き出し、そこから逆算するのが唯一の正解です。
設計時に selectivity を見積もる
「どの列が何件に絞り込むか」を設計時に概算しておくと、列順の判断がぶれません。実データがなくても、業務仕様から1 クエリあたり読み込まれる想定件数を見積もります。
| 列 | カーディナリティ | 典型クエリで絞られる件数 | 先頭候補 |
|---|---|---|---|
tenant_id | 数百テナント | 1 テナントで数万〜数百万件 | ◎(必ず先頭) |
user_id | 数万〜数百万 | 1 ユーザーで 1〜数千件 | ○(テナント配下で次点) |
status | 数種類 | 「active」で数十万件 | △(単独では弱い) |
created_at | 事実上無限 | 範囲条件で可変 | ×(末尾、ソート列として) |
絞り込みが一気に進む列を先頭に寄せることで、索引走査量が桁で減ります。tenant_id を先頭にするだけで 1/100 になる、user_id で次に絞って更に 1/1000、のようなイメージで組み立てます。
tuning 側との役割分担
本ページは 設計時にどう列順を決めるか に焦点を当てています。既に動いているクエリの実測・索引の入れ替え・EXPLAIN による答え合わせは、姉妹ページ 複合インデックスとカバリングインデックス(チューニング編) にまとめています。
設計レビューでは「このクエリを支える複合インデックスはどれか」を SQL ごとに確認する習慣を付けましょう。次ページでは索引にどの列まで含めれば「テーブル本体を読まずに完結」できるか(カバリング)の設計を扱います。
