既存のインデックスを使用していないクエリがあり、その理由がわかりません。
テーブル:
_mustang=# \d+ bss.amplifier_saturation
Table "bss.amplifier_saturation"
Column | Type | Modifiers | Storage | Description
--------+--------------------------+-------------------------------------------------------------------+---------+-------------
value | integer | not null | plain |
target | integer | not null | plain |
start | timestamp with time zone | not null | plain |
end | timestamp with time zone | not null | plain |
id | integer | not null default nextval('amplifier_saturation_id_seq'::regclass) | plain |
lddate | timestamp with time zone | not null default now() | plain |
Indexes:
"amplifier_saturation_pkey" PRIMARY KEY, btree (id)
"amplifier_saturation_target_start_end_key" UNIQUE CONSTRAINT, btree (target, start, "end")
"amplifier_saturation_end" btree ("end")
"amplifier_saturation_lddate" btree (lddate)
"amplifier_saturation_start" btree (start)
"amplifier_saturation_target" btree (target)
"amplifier_saturation_value" btree (value)
_
クエリ/プラン:
_mustang=# explain select max(lddate) from bss.amplifier_saturation
where start >= '1987-12-31 00:00:00'
and start <= '1988-04-09 00:00:00';
QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------------------------------------------
Result (cost=189.41..189.42 rows=1 width=0)
InitPlan 1 (returns $0)
-> Limit (cost=0.00..189.41 rows=1 width=8)
-> Index Scan Backward using amplifier_saturation_lddate on amplifier_saturation (cost=0.00..2475815.50 rows=13071 width=8)
Index Cond: (lddate IS NOT NULL)
Filter: ((start >= '1987-12-31 00:00:00-08'::timestamp with time zone) AND (start <= '1988-04-09 00:00:00-07'::timestamp with time zone))
_
なぜこれはインデックス_amplifier_saturation_start
_を使用しないのですか? DBが開始日を見つけるためにそれをスキャンし、終了日まですべてのエントリを区切るまで続行し、最後にそのlddate
( SQLパフォーマンスの説明のpp40-41のようなもの)。
私も必死に_(start, start desc)
_のインデックスを試しましたが、役に立ちませんでした。
ちなみに、select count(*)
は問題なく動作します。
_mustang=# explain select count(*) from bss.amplifier_saturation
where start >= '1987-12-31 00:00:00'
and start <= '1988-04-09 00:00:00';
QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------------------------
Aggregate (cost=38711.84..38711.85 rows=1 width=0)
-> Index Scan using amplifier_saturation_start on amplifier_saturation (cost=0.00..38681.47 rows=12146 width=0)
Index Cond: ((start >= '1987-12-31 00:00:00-08'::timestamp with time zone) AND (start <= '1988-04-09 00:00:00-07'::timestamp with time zone))
_
ANALYZE
を実行しても効果がありませんでした。pg_stats
_は、インデックスの使用を主張するように思われるstartの値の妥当な広がりを示しています。lddate
)で統計を10,000に設定しても、効果はありませんでした。多分私は計画が間違っていると思う理由を説明する必要があります。テーブルには,000,000行が含まれています。日付範囲には3,500のみです。しかし、多分それでも、それらを個別に読むには多すぎますか?
_(lddate desc, start)
_にインデックスを追加すると機能します(desc
が必要かどうかはわかりません)。次に、純粋なインデックスアプローチ(IIUC)を使用でき、muchより速く実行されます。
_mustang=# create index tmp_as on bss.amplifier_saturation (lddate desc, start);
CREATE INDEX
mustang=# explain select max(lddate) from bss.amplifier_saturation
where start >= '1987-12-31 00:00:00'
and start <= '1988-04-09 00:00:00';
QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Result (cost=69.76..69.77 rows=1 width=0)
InitPlan 1 (returns $0)
-> Limit (cost=0.00..69.76 rows=1 width=8)
-> Index Scan using tmp_as on amplifier_saturation (cost=0.00..861900.22 rows=12356 width=8)
Index Cond: ((lddate IS NOT NULL) AND (start >= '1987-12-31 00:00:00-08'::timestamp with time zone) AND (start <= '1988-04-09 00:00:00-07'::timestamp with time zone))
_
したがって、私自身の質問に答えると思います。3,500回データにアクセスするコストは、30,000,000値スキャンよりも遅いようです(ディスクを回転させます)。純粋なインデックススキャンは明らかに優れていますが。
多分私よりも賢い誰かがより良い答えを思いつくでしょうか?
私の質問は、なぜこれは_
amplifier_saturation_start
_インデックスを使用しないのですか?
_30,000,000 rows, only 3,500 in the date range
_を使用しても、lddate
のインデックス_amplifier_saturation_lddate
_の先頭からタプルを読み取る方が高速です。 start
でフィルターを通過する最初の行は、そのまま返すことができます。並べ替えの手順は必要ありません。完全にランダムな分布の場合、9000を少し下回るインデックスタプルをチェックする必要があります。
_amplifier_saturation_start
_を使用すると、Postgresは3500の条件を満たす行をすべてフェッチした後でも、max(lddate)
を決定する必要があります。通話を終了します。決定は、収集された統計と コスト設定 によって異なります。データの分布やその他の詳細に応じて、どちらか一方がより速くなり、どちらか一方が期待されますより速くなります。
multicolumn index を_(lddate, start)
_で使用すると、すでにお気づきのように、これはかなり高速になる可能性があります。このように、Postgresはインデックスのみのスキャンを使用でき、ヒープ(テーブル)にはまったく触れません。
しかし、改善できるもう1つのマイナー詳細があります。 EXPLAIN
の出力でこの詳細について疑問に思いましたか?
_Index Cond: ((lddate IS NOT NULL) AND ...
_
なぜPostgresはNULL値を除外しなければならないのですか?
NULLは後ASCENDING
の最大値または前DESCENDING
注文。集約関数max()
によって返されるnull以外の最大値は、NULL値がある場合、インデックスの先頭または末尾にありません。 _NULLS LAST | FIRST
_ を追加すると、並べ替え順序がmax()
の特性に調整されます(反対のmin()
がより高価になります)。 latestタイムスタンプに主に関心があるため、_DESC NULLS LAST
_の方が適しています。
CREATE INDEX tmp_as ON bss.amplifier_saturation (lddate DESC NULLS LAST, start);
これで、テーブルの列lddate
に_NOT NULL
_が定義されているため、明らかにNULL値がありません。この特定のケースでは、パフォーマンスへの影響は無視できます。 canにNULLがある可能性がある場合については、言及する価値があります。
他のインデックスオプションは_(start, lddate)
_にあり、基本的には単純な_amplifier_saturation_start
_インデックスで、インデックスのみのスキャンも可能です。クエリのデータ分布と実際のパラメーター値に応じて、どちらかが速くなります。
timestamp
に関する2つの注意事項テーブルの列はtimestamptz
ですが、クエリの述語はtimestamp
リテラルを使用します。 Postgresは現在のtimezone
設定からタイムゾーンを導き出し、それに応じて調整します。これは意図したとおりである場合とそうでない場合があります。それは確かにクエリをvolatileにします-セッションの設定に依存します。異なるタイムゾーンから(異なるセッション設定で)発信される可能性のある通話には問題があります。次に、明示的なオフセットまたは_AT TIME ZONE
_構成を使用してstableにします。詳細:
通常は、正確さの上限をexcludeする必要があります。 _<
_の代わりに_<=
_.
_select max(lddate)
from bss.amplifier_saturation
where start >= '1987-12-31 00:00:00'::timestamp AT TIME ZONE 'PST'
and start < '1988-04-09 00:00:00 PST'::timestamptz; -- shorter
_
PST
(太平洋標準時)はランダムなタイムゾーンの例です。