インデックスを作ったのに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期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?