インデックスが使われないときに見る 3 箇所
CREATE INDEX は通ったのに、EXPLAIN に Index Scan が出ない。
このとき闇雲に索引を足しても直りません。
疑う順序は決まっています。 上から順に潰していくと、たいてい 3 つ目までに当たります。
1. 統計情報が古い
プランナが見ているのはテーブルの中身ではありません。
ANALYZE が作った統計だけです。
統計に無い値は、何行あっても 1 行と見なされます。 1 行しか返らないつもりなら、索引を引く値段は安く見えます。逆に、消えたはずの値がまだ多いと思われていれば、索引は高く見えます。
まずここを疑います。
select last_analyze, last_autoanalyze, n_mod_since_analyze, n_live_tupfrom pg_stat_user_tableswhere relname = 'events';n_mod_since_analyze が n_live_tup に対して大きければ、統計は現実から離れています。
ANALYZE を走らせて EXPLAIN を取り直します。
大量に入れた直後、パーティションを付け替えた直後、一括削除の直後。 autovacuum の閾値 (既定で 10% + 50 行) に届くまでは統計が古いままです。
見積もりと実測を並べれば、統計が原因かどうかはすぐ分かります。
EXPLAIN ANALYZE の同じ行に出る 2 つの rows= を比べてください。左が見積もり、右が実測です。
これが原因なら ANALYZE で直ります。次の 2 つは直りません。
2. 型が暗黙に変換されている
列の型と比較値の型が違うと、Postgres は片方を変換します。 列の側が変換されると、その索引は使えません。
索引はもとの値で並んでいます。f(列) の結果では並んでいないので、引きようがありません。
実際に見てください。実行ボタンを押すと、この端末のブラウザの中で Postgres が起動します。
codes_n_idx が引かれます。
比較値を文字列で書いても同じです。where n = '42' は定数の側が int に変換されるだけで、列は素のままだからです。
列の側を変換すると変わります。
計画から索引が消えて、全表走査に落ちます。
n::text は列に関数を掛けた形です。索引は n の順に並んでいて、n::text の順には並んでいません。
ただし、すべてのキャストが索引を殺すわけではありません。
これは索引が残ります。
varchar から text は内部表現が同じ (binary coercible) なので、Postgres は変換そのものを省くためです。
現場でよく出るのはこの形です。
| 書き方 | 何が起きるか |
|---|---|
where n = '42' | 定数側が変換される。索引は効く |
where n::text = '42' | 列側が変換される。索引が効かない |
where code::text = '...' | 内部表現が同じなので変換されない。索引は効く |
where date(created_at) = '2026-01-01' | 列側に関数。索引が効かない |
where lower(name) = 'x' | 列側に関数。索引が効かない |
キャストを見ただけで判断しないでください。 効くかどうかは EXPLAIN に出ます。
下 3 つを索引で引きたいなら、式そのものに索引を張ります。
create index on events (date(at));create index on users (lower(name));date(created_at) = ... は、範囲条件に書き換えても索引が効きます。
where created_at >= '2026-01-01' and created_at < '2026-01-02'書き換えられるなら、式索引より範囲条件を選んでください。 索引が 1 本減ります。
3. 選択率が高い
統計も型も問題ないのに使われない。 このときは、プランナが正しく判断している可能性を先に疑ってください。
索引走査は、索引を引いてから本体のページを取りに行きます。 条件に合う行が多いと、結局ほとんどのページを読むことになります。それなら最初から順に読んだほうが安い、という判断です。
分かれ目は「何 % の行が条件を通るか」です。
この境目は環境で動きます。random_page_cost が小さいほど、effective_cache_size が大きいほど、索引は高い選択率まで使われます。
見分けるのは Buffers です。
Bitmap Heap Scan の Heap Blocks がテーブルの全ページ数と一致していたら、索引を引いた末に全ページ読んでいます。索引が効いていないのと同じです。
境目を自分で動かして交点を探すところは、RDBMS のクエリ実行を理解する でやっています。
それでも合わないとき
3 つとも外れたら、次を疑います。
| 症状 | 疑う先 |
|---|---|
| 複合索引の 2 列目だけで検索している | 先頭列が条件に無いと引けない |
ORDER BY と索引の並び順が違う | DESC / NULLS LAST を索引側に合わせる |
LIKE '%x' で始めている | 前方一致でないと B-tree では引けない |
| 照合順序が索引と違う | COLLATE が違うと別の索引として扱われる |
| 見積もりは合っているのに計画が悪い | 値の偏り。Rows Removed by Filter を見る |
最後のひとつは統計では直りません。
プランナは、条件に合う行が索引の並びに均等に散らばっていると仮定します。偏っていると、LIMIT の付いたクエリで大きく外します。