web-dev-qa-db-ja.com

日付範囲クエリで未使用のインデックス

既存のインデックスを使用していないクエリがあり、その理由がわかりません。

テーブル:

_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の値の妥当な広がりを示しています。
  • いずれかの列(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値スキャンよりも遅いようです(ディスクを回転させます)。純粋なインデックススキャンは明らかに優れていますが。

多分私よりも賢い誰かがより良い答えを思いつくでしょうか?

5
andrew cooke

説明

私の質問は、なぜこれは_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つの注意事項

  1. テーブルの列はtimestamptzですが、クエリの述語はtimestampリテラルを使用します。 Postgresは現在のtimezone設定からタイムゾーンを導き出し、それに応じて調整します。これは意図したとおりである場合とそうでない場合があります。それは確かにクエリをvolatileにします-セッションの設定に依存します。異なるタイムゾーンから(異なるセッション設定で)発信される可能性のある通話には問題があります。次に、明示的なオフセットまたは_AT TIME ZONE_構成を使用してstableにします。詳細:

  2. 通常は、正確さの上限を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(太平洋標準時)はランダムなタイムゾーンの例です。

9