Query Go
複合インデックスの列順設計 — 等値 → 範囲 → ソート
データモデル設計ガイド

複合インデックスの列順設計 — 等値 → 範囲 → ソート

複合インデックスの列順は「等値 → 範囲 → ソート」。左端一致の再利用とよくある失敗を押さえる。

複合インデックスの列順設計 — 等値 → 範囲 → ソート diagram

原則: 等値 → 範囲 → ソート

複合インデックスの列順は、次の 3 ステップの順に並べるのが基本原則です。この順序を外すとインデックスが使われない・使われても遅い状況に陥ります。

  1. 等値検索で使う列WHERE col = ?)を先頭に
  2. 続けて範囲検索で使う列>=, BETWEEN, <, IN
  3. 最後に 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 本貼るより索引本数が少なく、書き込みコストも抑えられます。

sql
-- 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 ごとに確認する習慣を付けましょう。次ページでは索引にどの列まで含めれば「テーブル本体を読まずに完結」できるか(カバリング)の設計を扱います。