本文へスキップ

記事postgres

インデックスが使われないときに見る 3 箇所

CREATE INDEX は通ったのに、EXPLAINIndex Scan が出ない。 このとき闇雲に索引を足しても直りません。

疑う順序は決まっています。 上から順に潰していくと、たいてい 3 つ目までに当たります。

1. 統計情報が古い

プランナが見ているのはテーブルの中身ではありません。 ANALYZE が作った統計だけです。

統計に無い値は、何行あっても 1 行と見なされます。 1 行しか返らないつもりなら、索引を引く値段は安く見えます。逆に、消えたはずの値がまだ多いと思われていれば、索引は高く見えます。

まずここを疑います。

select last_analyze, last_autoanalyze, n_mod_since_analyze, n_live_tup
from pg_stat_user_tables
where relname = 'events';

n_mod_since_analyzen_live_tup に対して大きければ、統計は現実から離れています。 ANALYZE を走らせて EXPLAIN を取り直します。

大量に入れた直後、パーティションを付け替えた直後、一括削除の直後。 autovacuum の閾値 (既定で 10% + 50 行) に届くまでは統計が古いままです。

見積もりと実測を並べれば、統計が原因かどうかはすぐ分かります。 EXPLAIN ANALYZE の同じ行に出る 2 つの rows= を比べてください。左が見積もり、右が実測です。

これが原因なら ANALYZE で直ります。次の 2 つは直りません。

2. 型が暗黙に変換されている

列の型と比較値の型が違うと、Postgres は片方を変換します。 列の側が変換されると、その索引は使えません。

索引はもとの値で並んでいます。f(列) の結果では並んでいないので、引きようがありません。

実際に見てください。実行ボタンを押すと、この端末のブラウザの中で Postgres が起動します。

2. 型が暗黙に変換されている

codes_n_idx が引かれます。 比較値を文字列で書いても同じです。where n = '42'定数の側int に変換されるだけで、列は素のままだからです。

列の側を変換すると変わります。

2. 型が暗黙に変換されている

計画から索引が消えて、全表走査に落ちます。 n::text は列に関数を掛けた形です。索引は n の順に並んでいて、n::text の順には並んでいません。

ただし、すべてのキャストが索引を殺すわけではありません。

2. 型が暗黙に変換されている

これは索引が残ります。 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 ScanHeap Blocks がテーブルの全ページ数と一致していたら、索引を引いた末に全ページ読んでいます。索引が効いていないのと同じです。

境目を自分で動かして交点を探すところは、RDBMS のクエリ実行を理解する でやっています。

それでも合わないとき

3 つとも外れたら、次を疑います。

症状疑う先
複合索引の 2 列目だけで検索している先頭列が条件に無いと引けない
ORDER BY と索引の並び順が違うDESC / NULLS LAST を索引側に合わせる
LIKE '%x' で始めている前方一致でないと B-tree では引けない
照合順序が索引と違うCOLLATE が違うと別の索引として扱われる
見積もりは合っているのに計画が悪い値の偏り。Rows Removed by Filter を見る

最後のひとつは統計では直りません。 プランナは、条件に合う行が索引の並びに均等に散らばっていると仮定します。偏っていると、LIMIT の付いたクエリで大きく外します。

関連

2026.08.21RDBMS のクエリ実行を理解するハンズオン