本文へスキップ

記事postgres

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

CREATE INDEX は成功したのに、EXPLAIN に Index 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_analyze は前回の ANALYZE の後に変更された行数である。n_live_tup に占める n_mod_since_analyze の割合が大きいほど、統計は今のテーブルの行と違っている。ANALYZE を実行して、EXPLAIN を取り直す。

大量の行を入れた直後、パーティションを付け替えた直後、一括で削除した直後は、統計が変更の前のままである。autovacuum は、変更された行数が閾値 (デフォルトで 10% + 50 行) に達するまで ANALYZE を実行しない。

見積もりと実際の行数を比べると、統計が原因かどうかが分かる。EXPLAIN ANALYZE の同じ行に出る 2 つの rows= を比べる。左が見積もりの行数、右が実際の行数である。

統計が古いことが原因なら、ANALYZE で統計を作り直せば直る。次の 2 つの原因は、ANALYZE では直らない。

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

列の型と比較値の型が違うと、Postgres はどちらか一方の型を変換する。列の側が変換されると、その列のインデックスは使えない。

インデックスは、列のもとの値の順に並んでいる。f(列) の結果の順には並んでいないので、f(列) の値ではインデックスを探索できない。

下の実行パネルで SQL を実行して確かめる。

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

計画で codes_n_idx が使われる。比較値を文字列で書いても、codes_n_idx が使われる。where n = '42' では、定数の側が int に変換されるだけで、列は変換されないからである。

列の側を text に変換すると、計画が変わる。

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

計画から codes_n_idx が消え、テーブル全体を順に読む Seq Scan になる。n::text は、列 n に型変換を適用した式である。インデックスは n の順に並んでいて、n::text の順には並んでいない。

列の側のキャストでも、インデックスを使えるものがある。

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

この計画では、主キー code のインデックスが使われる。varchar と text は内部表現が同じ (binary coercible) なので、Postgres は varchar から text への変換を省くからである。

条件のよくある書き方と、インデックスを使えるかどうかは次のとおり。

書き方何が起きるか
where n = '42'定数の側が変換される。インデックスを使える
where n::text = '42'列の側が変換される。インデックスを使えない
where code::text = '...'内部表現が同じなので変換されない。インデックスを使える
where date(created_at) = '2026-01-01'列の側に関数を適用する。インデックスを使えない
where lower(name) = 'x'列の側に関数を適用する。インデックスを使えない

キャストを書いたかどうかだけでは、インデックスを使えるかどうかは決まらない。EXPLAIN の計画で確かめる。

表の下の 2 つ (date(created_at) と lower(name)) の条件でインデックスを使いたいなら、式そのものにインデックスを作る。

create index on users (lower(name));
create index on events (((created_at at time zone 'Asia/Tokyo')::date));

created_at が timestamptz のとき、date(created_at) の結果は TimeZone の設定によって変わる。そのため date(created_at) は IMMUTABLE ではなく、そのままの式ではインデックスを作れない。上の例のようにタイムゾーンを固定した式でインデックスを作り、クエリの条件も同じ式で書く。

date(created_at) = ... は、範囲条件に書き換えると、created_at のインデックスを使える。

where created_at >= '2026-01-01' and created_at < '2026-01-02'

範囲条件に書き換えられるなら、式インデックスを作らずに範囲条件を使う。created_at のインデックスを使うので、式インデックスの 1 本が要らなくなる。

3. 選択率が高い

統計が新しく、型の変換も起きていないのに、インデックスが使われないことがある。このときは、インデックスを使わない計画のほうがコストが低いとプランナが見積もっている可能性を先に考える。

Index Scan は、インデックスを読んでから、テーブル本体のページを読む。条件に合う行が多いと、テーブルのほとんどのページを読むことになる。その場合は、インデックスを読まずにテーブルを先頭から順に読む Seq Scan のほうがコストが低い、とプランナは見積もる。

どちらの計画を選ぶかは、テーブルの全行のうち条件に合う行の割合 (選択率) で決まる。インデックスを使う計画に切り替わる選択率は、設定によって変わる。random_page_cost が小さいほど、effective_cache_size が大きいほど、インデックスは高い選択率まで使われる。

選択率が原因かどうかは、EXPLAIN (ANALYZE, BUFFERS) の出力の Buffers を見て見分ける。Bitmap Heap Scan の Heap Blocks がテーブルの全ページ数と一致していたら、インデックスを読んだうえで全ページを読んでいる。読むページ数は、インデックスを使わない場合と変わらない。

選択率を変えて、計画が切り替わる選択率を探す手順は RDBMS のクエリ実行を理解する にある。

それでも使われないとき

3 つの原因のどれにも当てはまらなければ、次の表の症状を確かめる。

症状原因と対処
複合インデックスの 2 列目だけで検索している先頭の列が条件に無いと使えない
ORDER BY とインデックスの並び順が違うDESC / NULLS LAST をインデックスの側に合わせる
LIKE '%x' で始めている前方一致でないと B-tree では探せない
COLLATE がインデックスと違うCOLLATE が違うインデックスは使われない
見積もりは合っているのに計画が悪い値の偏り。Rows Removed by Filter を見る

表の最後の行の値の偏りは、ANALYZE で統計を作り直しても直らない。プランナは、条件に合う行がインデックスの並びの中に均等に分布していると仮定する。条件に合う行がインデックスの並びの後ろのほうに集まっていると、LIMIT の付いたクエリでは、条件に合う行が見つかるまでに読む行数が見積もりより多くなる。

関連

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