- データベース
- 技術 💻
SQLのインデックスが効かない10のパターンと確認方法
インデックスを作ったのにSQLが速くならない主な原因は、「インデックスを作った列を、関数や計算で加工して条件にしている」「LIKEの先頭にワイルドカード(%)を付けている」「複合インデックスの先頭の列を条件に含めていない」といった、SQLの書き方にあります。インデックスは、列の値そのものを並べた索引なので、値を加工したり、途中から探したりすると使えなくなるのです。
この記事では、インデックスが効かない10のパターンと、それぞれの書き換え方を、PostgreSQL 16で100万件のテーブルを作り、実際に確認した実行計画とあわせて解説します。EXPLAINの読み方や、MySQLとの違い、インデックスを作りすぎたときの注意点も紹介します。
インデックスが効いているかの確認方法:EXPLAIN
まず、インデックスが使われているかを確認する方法です。SQLの前に EXPLAIN を付けると、DBがどのように実行するか(実行計画)が表示されます。
EXPLAIN SELECT * FROM orders WHERE customer_id = 5;
| DB | インデックスが使われている | 使われていない(全件を読む) |
|---|---|---|
| PostgreSQL | Index Scan、Index Only Scan、Bitmap Index Scan | Seq Scan |
| MySQL | type が ref・range・eq_ref・const、key にインデックス名 | type が ALL、key が NULL |
PostgreSQLでは EXPLAIN ANALYZE、MySQL 8.0.18以降では EXPLAIN ANALYZE で、実際に実行したときの時間も確認できます(実際にSQLが実行されるため、更新系のSQLには注意します)。
インデックスが効かない10のパターン
以下の実行計画は、100万件の注文テーブル(orders)に、各列のインデックスを作成して確認したものです。
1. 列に関数を使っている
-- × 効かない
SELECT * FROM orders WHERE date(created_at) = '2026-01-01';
-- → Parallel Seq Scan on orders Filter: (date(created_at) = '2026-01-01')
-- ○ 効く:列をそのままにして、範囲で書く
SELECT * FROM orders
WHERE created_at >= '2026-01-01' AND created_at < '2026-01-02';
-- → Bitmap Index Scan on orders_created_at_idx
インデックスに入っているのは created_at の値そのもので、date(created_at) の値ではありません。関数をかけた結果で探そうとすると、全件に関数を適用して比べることになります。日付で絞り込むときは、範囲の条件に書き換えるのが定番です。
2. 列に計算をしている
-- × 効かない
SELECT * FROM orders WHERE amount + 100 > 9990;
-- → Seq Scan on orders Filter: ((amount + 100) > 9990)
-- ○ 効く:計算を右辺に移す
SELECT * FROM orders WHERE amount > 9890;
-- → Bitmap Index Scan on orders_amount_idx
計算は列の側ではなく、値の側で行うようにします。
3. 大文字・小文字を区別せずに検索している
-- × 通常のインデックスでは効かない
SELECT * FROM orders WHERE lower(email) = 'user5@example.com';
-- → Parallel Seq Scan
-- ○ 式インデックスを作る
CREATE INDEX ON orders (lower(email));
-- → Bitmap Index Scan on orders_lower_idx
関数を使った検索がどうしても必要な場合は、関数の結果に対するインデックス(式インデックス)を作ります。MySQLでも8.0.13以降、関数インデックス(CREATE INDEX idx ON orders ((lower(email))))が使えます。保存時に小文字に揃えておく方法もあります。
4. LIKEの先頭にワイルドカードを付けている
-- × 効かない(途中一致・後方一致)
SELECT * FROM orders WHERE email LIKE '%@example.com';
-- ○ 前方一致なら効く可能性がある
SELECT * FROM orders WHERE email LIKE 'user54321%';
インデックスは、辞書のように先頭の文字から順に並んでいます。「〜で終わる」「〜を含む」という探し方では、どこから探せばよいか決まらないため、全件を調べることになります。
PostgreSQLの注意点:データベースの照合順序(ロケール)がC以外(ja_JP.UTF-8やen_US.UTF-8など)の場合、通常のインデックスでは前方一致のLIKEでも使われません。実際に確認すると、LIKE 'user54321%' はSeq Scanでした。text_pattern_ops を指定したインデックスを作ると、使われるようになります。
CREATE INDEX ON orders (email text_pattern_ops);
-- → Index Scan using orders_email_pattern
-- Index Cond: ((email ~>=~ 'user54321') AND (email ~<~ 'user54322'))
途中一致の検索が必要な場合は、PostgreSQLの pg_trgm 拡張(トライグラムインデックス)や、MySQLの全文検索インデックス、Elasticsearchなどの検索エンジンを検討します。
5. 型が違う値と比べている(暗黙の型変換)
-- code は文字列(varchar)の列
-- × 効かない:列の側を数値に変換している
SELECT * FROM orders WHERE code::int = 123;
-- → Parallel Seq Scan Filter: ((code)::integer = 123)
-- ○ 効く:値の側を列の型に合わせる
SELECT * FROM orders WHERE code = '000123';
MySQLでは特に注意が必要です。文字列の列を数値と比べる(WHERE code = 123)と、エラーにならずに列の側が数値に変換され、インデックスが使われません。郵便番号や社員番号のように、数字だけでも文字列の列には、必ず文字列(クォートで囲んだ値)で比べます。アプリケーションから値を渡すときも、型を合わせます。
JOINの結合条件で、両方の列の型や文字コード・照合順序が違う場合も、同じ理由でインデックスが使われないことがあります。
6. 複合インデックスの先頭の列を条件に含めていない
CREATE INDEX ON orders (customer_id, status);
-- ○ 効く:先頭の列(customer_id)を使っている
SELECT * FROM orders WHERE customer_id = 5;
-- → Bitmap Index Scan on orders_customer_id_status_idx
-- × 効かない:2番目の列(status)だけで検索している
SELECT * FROM orders WHERE status = 'paid';
-- → Seq Scan on orders
複合インデックスは、先頭の列で並べ、同じ値の中で2番目の列で並べた索引です。電話帳が「名字→名前」の順に並んでいるとき、名前だけでは探せないのと同じです。
複合インデックスの列の順番の決め方は、次のとおりです。
- 等号(=)で検索する列を前に、範囲(>、<、BETWEEN)で検索する列を後ろに
- よく単独でも検索する列を前に
- 範囲条件の列より後ろの列は、絞り込みに使われにくい
MySQL 8.0.13以降やPostgreSQL 18以降では、先頭の列の種類が少ない場合に、2番目の列だけの条件でもインデックスを使える「スキップスキャン」が実装されています。ただし、常に使われるわけではないため、先頭の列を条件に含めるのが基本です。
7. 否定の条件(<>、!=、NOT IN)を使っている
SELECT * FROM orders WHERE amount <> 5;
-- → Seq Scan on orders(約100万件のほぼすべてが該当)
否定の条件は、該当する行がほとんど全件になることが多いため、インデックスを使うより全件を読む方が速いと判断されます。「〜以外」ではなく、該当する値を列挙する(IN)、または状態を表す列の設計を見直すことを検討します。
8. 条件に合う行が多すぎる(選択性が低い)
SELECT * FROM orders WHERE amount > 10;
-- → Seq Scan on orders(推定999,003行)
インデックスが使える書き方でも、条件に合う行がテーブルの大部分を占める場合、DBはインデックスを使いません。インデックスを経由して1行ずつ取りに行くより、テーブルを最初から順に読む方が速いためです。
「性別」「削除フラグ」のように値の種類が少ない列に単独でインデックスを作っても、効果が出にくいのは同じ理由です。こうした列は、複合インデックスの一部として使うか、PostgreSQLなら部分インデックス(WHERE deleted = false の行だけのインデックス)を検討します。
9. ORDER BYの並べ替えで、列を加工している
-- ○ 効く:インデックスの順に読み、先頭10件で止まる
SELECT * FROM orders ORDER BY created_at DESC LIMIT 10;
-- → Limit → Index Scan Backward using orders_created_at_idx
-- × 効かない:加工した値で並べ替えている
SELECT * FROM orders ORDER BY amount + 0 DESC LIMIT 10;
-- → Sort → Parallel Seq Scan(全件を読んで並べ替え)
インデックスは値が並んだ状態で保存されているため、ORDER BYの列にインデックスがあれば、並べ替えを省略し、LIMITの件数で読み込みを止められます。列を加工すると、この効果が得られません。
10. ORの条件で、片方の列にインデックスがない
SELECT * FROM orders WHERE customer_id = 5 OR email = 'user5@example.com';
-- → BitmapOr(両方の列のインデックスを組み合わせて使用)
PostgreSQLでは、ORの両方の列にインデックスがあれば、それぞれを使って結果を合わせる(BitmapOr)ことができます。片方にでもインデックスがないと、全件を読むことになります。MySQLでも「インデックスマージ」という仕組みがありますが、使われない場合は UNION ALL に分けて書くと、それぞれでインデックスを使えます。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「SQLのインデックスが効かない10のパターンと確認方法」
早見表:効かない書き方と書き換え方
| 効かない書き方 | 書き換え方 |
|---|---|
WHERE date(created_at) = '2026-01-01' |
WHERE created_at >= '2026-01-01' AND created_at < '2026-01-02' |
WHERE amount + 100 > 9990 |
WHERE amount > 9890 |
WHERE lower(email) = '...' |
式インデックスを作る、または保存時に小文字に揃える |
WHERE email LIKE '%@example.com' |
前方一致にする、全文検索・トライグラムインデックスを使う |
WHERE code = 123(codeは文字列) |
WHERE code = '123'(型を合わせる) |
複合インデックス (a, b) で WHERE b = ... |
先頭の列 a を条件に含める、または (b) のインデックスを作る |
WHERE status <> 'paid' |
WHERE status IN ('cancel', 'pending') |
WHERE a = 1 OR b = 2(bにインデックスなし) |
両方にインデックスを作る、UNION ALLに分ける |
ORDER BY 加工した値 |
列そのもので並べ替える |
WHERE substring(code, 1, 3) = 'ABC' |
WHERE code LIKE 'ABC%' |
書き方が正しいのに使われない場合
統計情報が古い
DBは、テーブルの件数や値の分布(統計情報)をもとに、インデックスを使うかどうかを判断します。大量のデータを投入した直後などは統計情報が古く、誤った判断をすることがあります。
ANALYZE orders; -- PostgreSQL
ANALYZE TABLE orders; -- MySQL
テーブルが小さい
数百行程度の小さなテーブルでは、インデックスを使うより全件を読む方が速いため、Seq Scan(type: ALL)になるのが正常です。本番と同じくらいのデータ量で確認しないと、判断を誤ります。
インデックスは作りすぎにも注意
インデックスは検索を速くしますが、INSERT・UPDATE・DELETEのたびに、インデックスも更新する必要があります。
- インデックスが多いほど、書き込みが遅くなる
- インデックスの分だけ、ディスクとメモリを使う
- 似たインデックスが複数あると、DBが誤ったものを選ぶことがある
複合インデックス (a, b) があれば、a だけのインデックスは基本的に不要です。実際に使われているSQLを確認してから作る、使われていないインデックスは定期的に見直す、という運用が大切です。
よくある質問
Q. IS NULL の条件でインデックスは使われますか? A. PostgreSQLとMySQLでは、IS NULLでもインデックスが使われることがあります。Oracleの通常のインデックスはNULLだけの行を含まないため、使われない場合があります。DBによって違うため、EXPLAINで確認するのが確実です。
Q. 主キーにはインデックスを作る必要がありますか? A. 主キーやユニーク制約を設定すると、多くのDBで自動的にインデックスが作られます。別に作る必要はありません。一方、外部キーの列には自動で作られないDB(PostgreSQLなど)もあるため、JOINに使う場合は作成を検討します。
Q. どの列にインデックスを作ればいいですか? A. WHERE・JOIN・ORDER BYでよく使われ、条件によって行が十分に絞り込まれる列が候補です。遅いSQLのログ(スロークエリログ)から、実際に時間がかかっているSQLを見つけて対策するのが効率的です。
Q. インデックスを作ったらSQLが遅くなりました。 A. DBが新しいインデックスを使った結果、かえって遅い実行計画になることがあります。EXPLAIN ANALYZEで実際の時間を比較し、統計情報の更新や、インデックスの見直しを行います。
まとめ
- インデックスが効かない主な原因は、列の加工(関数・計算・型変換)と、先頭から探せない条件
- 日付は範囲で、計算は値の側で、型は列に合わせて書く
- LIKEは前方一致のみ。PostgreSQLでは照合順序によって text_pattern_ops が必要
- 複合インデックスは先頭の列から使われる。等号の列を前、範囲の列を後ろに
- 該当する行が多すぎると、正しい書き方でも全件を読む。EXPLAINで必ず確認する
- インデックスは書き込みの負担にもなる。使われているSQLを確認してから作る
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「SQLのインデックスが効かない10のパターンと確認方法」
株式会社RJCでは、「なんとなく遅い」を実行計画で確かめて改善する姿勢を大切にしています。こうした知識を仲間と共有しながら、一緒に成長を楽しんでいます。
株式会社RJC ― SI事業・SES事業・AI駆動開発。未経験からエンジニアに育てるのが得意な会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。