doodle-on-web

自分で調べたことや、仕事の中で質問されたことなどをまとめています。

遅いクエリを即改善!SQLインデックス設計の基本と落とし穴【実行計画付き】

スポンサーリンク


「クエリが遅い」「DBサーバーの負荷が高い」——開発現場でこんな問題に直面したことはありませんか?

筆者が過去に担当したECサイトでは、注文一覧の取得に8秒以上かかっていたクエリが、インデックスの見直しだけで0.05秒未満に改善した経験があります。コードを1行も変えずに、です。

この記事を読み終えれば、EXPLAINコマンドを使って自分でボトルネックを特定し、根拠のあるインデックス設計ができるようになります。「なんとなく設定」から卒業しましょう。


まず「EXPLAIN」でボトルネックを特定する

インデックスを闇雲に追加する前に、現状把握が最優先です。MySQLやPostgreSQLにはEXPLAINコマンドが用意されており、クエリがどのように実行されているかを確認できます。

EXPLAIN SELECT * FROM orders WHERE user_id = 100 AND order_date >= '2024-01-01';

出力結果のどこを見ればよいか、重要な項目を絞って紹介します。

項目 見るべき内容
type ALL(フルスキャン)は要注意。refrangeを目指す
key 実際に使用されているインデックス名。NULLならインデックス未使用
rows スキャン行数の推定値。少ないほど良い
Extra Using indexは高速の証。Using filesortはソート処理が発生している証拠

type: ALLかつrowsが数十万以上なら、インデックスの設計を見直す余地が大いにあります。


インデックスの基本:なぜ速くなるのか

インデックスとは、テーブルのデータを高速に検索するための「索引」です。インデックスがない場合、DBはテーブルの全行を先頭から順番に走査します(フルテーブルスキャン)。データが数百万件になると、これだけで数秒〜数十秒かかることがあります。

インデックスを設定すると、DBは目的のデータに直接アクセスできるようになり、検索速度が劇的に向上します。

代表的なインデックスの種類

B-Treeインデックス(デフォルト) MySQL・PostgreSQL・Oracleなど主要なRDBMSでデフォルトとして使われます。木構造でデータを管理し、等値検索・範囲検索・ソートのすべてに対応できる万能型です。特別な理由がなければこれを使います。

CREATE INDEX idx_users_email ON users(email);

ハッシュインデックス 等値検索(=)に限定した超高速インデックスです。ただしBETWEEN>などの範囲検索には使えません。「IDによるピンポイント検索しか行わない」といった用途に限定されます。

複合インデックス 複数カラムを組み合わせたインデックスです。設計次第でパフォーマンスを大きく左右するため、後のセクションで詳しく解説します。


インデックスを設定すべきカラムの見極め方

インデックスは「多ければ多いほど良い」ものではありません。インデックスが増えるほどINSERTUPDATEDELETEのコストが増加し、ストレージ消費も増えます。以下の基準で対象を絞り込みましょう。

✅ インデックスに向いているカラム

  • WHERE句に頻繁に登場するカラム(例:statususer_id
  • JOIN条件に使われるカラム(外部キーなど)
  • ORDER BYGROUP BYに使われるカラム
  • カーディナリティ(値の種類数)が高いカラム(メールアドレス、UUIDなど)

❌ インデックスに向いていないカラム

  • カーディナリティが低いカラム(true/falseのフラグ、性別など)
  • 頻繁に更新されるカラム
  • レコード数が数千件以下の小さなテーブルのカラム

複合インデックスの設計:カラム順序が命

複合インデックスの設計で最も重要なのが「カラムの順序」です。2つの原則を順番に理解しましょう。

原則①:左端一致(絶対条件)

複合インデックス(A, B, C)は、左端のカラムから順に使われる場合にのみ有効です。

-- ✅ 有効:左端のAを含む
WHERE A = 1
WHERE A = 1 AND B = 2
WHERE A = 1 AND B = 2 AND C = 3

-- ❌ 無効:左端のAが抜けている
WHERE B = 2
WHERE B = 2 AND C = 3

原則②:カーディナリティの高い順(次の判断軸)

左端一致を守ったうえで、絞り込み効果の高いカラム(値の種類が多いカラム)を左に配置すると、インデックスの効率がさらに上がります。

-- ✅ 推奨:カーディナリティの高いuser_idを左に
CREATE INDEX idx_orders_user_status ON orders(user_id, status);

-- ⚠️ 非推奨:カーディナリティの低いstatusを左に
CREATE INDEX idx_orders_status_user ON orders(status, user_id);

まず「どのカラムが左端に来るか」を決め、その後「カーディナリティ」で順序を微調整する、という判断フローで設計するとミスが減ります。


よくあるアンチパターンと対策

インデックスを設定しても「効いていない」ケースの多くは、以下のいずれかが原因です。

❌ インデックスカラムに関数を使う

-- ❌ 関数でラップするとインデックスが無効に
WHERE YEAR(created_at) = 2024

-- ✅ 範囲検索に書き換える
WHERE created_at >= '2024-01-01' AND created_at < '2025-01-01'

❌ 暗黙の型変換

-- ❌ カラムがINT型なのに文字列で比較すると型変換が発生
WHERE user_id = '100'

-- ✅ 型を合わせる
WHERE user_id = 100

❌ LIKE検索の後方・中間一致

-- ❌ 前方一致以外はインデックスが効かない
WHERE name LIKE '%田中%'

-- ✅ 前方一致ならインデックスが使える
WHERE name LIKE '田中%'

中間・後方一致の全文検索が必要な場合は、MySQLのFULLTEXT INDEXやElasticsearchなど専用の仕組みを検討しましょう。


まとめ:今日から使えるインデックス設計の鉄則

# 鉄則
1 まずEXPLAINでボトルネックを特定する
2 WHEREJOINORDER BYに使うカラムを優先的にインデックス化する
3 複合インデックスは左端一致を絶対条件として、カーディナリティ順に並べる
4 カーディナリティの低いカラム単体にインデックスは不要
5 インデックスカラムに関数・型変換を加えない
6 定期的に不要なインデックスをSHOW INDEXpg_stat_user_indexesで見直す

インデックス設計に「魔法」はありません。しかし、EXPLAINで現状を把握し、上記の原則に従って設計するだけで、クエリの実行速度は確実に改善できます。まず手元のスロークエリにEXPLAINを打ち込むところから始めてみてください。