web-dev-qa-db-ja.com

単純な時間範囲結合にインデックスを使用するにはどうすればよいですか?

Postgres 11.5に大きな(〜1億行)timeseriesテーブル_t_16_があり、主キーはフィールド_abs_date_time_ですタイプはtimestampです。

これはこの質問のフォローアップです:

最初はCTEに関連していると思いました。ただし、thisクエリは、CTEがなくても低速です。

How次のクエリを実行できますか主キーインデックスを使用して、全表スキャンを避けますか?

tsrange

このクエリは、私の開発用PCで〜20秒かかります:

_SELECT t_16_gen.*
FROM t_16_gen,
     (VALUES (tsrange('["2019-11-26 12:00:00","2019-11-26 12:00:15")'))
           , (tsrange('["2019-11-26 13:00:00","2019-11-26 13:00:15")'))) as ranges (time_range)
WHERE (abs_date_time >= LOWER(ranges.time_range)
    AND abs_date_time <  UPPER(ranges.time_range));
_

計画を説明する:

_Gather  (cost=1000.00..6185287.15 rows=20571433 width=80)
  Workers Planned: 2
  ->  Nested Loop  (cost=0.00..4127143.85 rows=8571430 width=80)
        Join Filter: ((t_16_gen.abs_date_time >= lower("*VALUES*".column1)) AND (t_16_gen.abs_date_time < upper("*VALUES*".column1)))
        ->  Parallel Seq Scan on t_16_gen  (cost=0.00..1620000.38 rows=38571438 width=80)
        ->  Values Scan on "*VALUES*"  (cost=0.00..0.03 rows=2 width=32)
_

本番環境では、tsrangeのセットはUDFから取得されますが、常に数個の範囲(<200)しかなく、各範囲は1500行未満であり、範囲は重複しません。

tsrangeの代わりに単純なタイムスタンプ

タイムスタンプを直接使用する場合(つまり、tsrange、LOWER()およびUPPER()を使用しない場合)、クエリはすでに高速です。このクエリは、開発用PCで〜7秒かかります。

_SELECT t_16_gen.*
FROM t_16_gen,
     (VALUES ('2019-11-26 12:00:00'::timestamp,'2019-11-26 12:00:15'::timestamp)
           , ('2019-11-26 13:00:00','2019-11-26 13:00:15')) as ranges (start_incl, end_excl)
WHERE (abs_date_time >= ranges.start_incl
    AND abs_date_time <  ranges.end_excl);
_

計画を説明する:

_Nested Loop  (cost=0.00..5400001.28 rows=20571433 width=80)
  Join Filter: ((t_16_gen.abs_date_time >= "*VALUES*".column1) AND (t_16_gen.abs_date_time < "*VALUES*".column2))
  ->  Seq Scan on t_16_gen  (cost=0.00..2160000.50 rows=92571450 width=80)
  ->  Materialize  (cost=0.00..0.04 rows=2 width=16)
        ->  Values Scan on "*VALUES*"  (cost=0.00..0.03 rows=2 width=16)
_

OR条件=高速

[〜#〜]または[〜#〜]条件を使用するようにクエリを書き換えると、高速になります。このクエリは、開発用PCで〜200msかかります:

_SELECT t_16_gen.*
FROM t_16_gen
WHERE (abs_date_time >= '2019-11-26 12:00:00' AND abs_date_time < '2019-11-26 12:00:15')
   OR (abs_date_time >= '2019-11-26 13:00:00' AND abs_date_time < '2019-11-26 13:00:15');
_

計画を説明する:

_Gather  (cost=13326.98..1533350.92 rows=923400 width=80)
  Workers Planned: 2
  ->  Parallel Bitmap Heap Scan on t_16_gen  (cost=12326.98..1440010.92 rows=384750 width=80)
        Recheck Cond: (((abs_date_time >= '2019-11-26 12:00:00'::timestamp without time zone) AND (abs_date_time < '2019-11-26 12:00:15'::timestamp without time zone)) OR ((abs_date_time >= '2019-11-26 13:00:00'::timestamp without time zone) AND (abs_date_time < '2019-11-26 13:00:15'::timestamp without time zone)))
        ->  BitmapOr  (cost=12326.98..12326.98 rows=925714 width=0)
              ->  Bitmap Index Scan on t_16_pkey  (cost=0.00..5932.64 rows=462857 width=0)
                    Index Cond: ((abs_date_time >= '2019-11-26 12:00:00'::timestamp without time zone) AND (abs_date_time < '2019-11-26 12:00:15'::timestamp without time zone))
              ->  Bitmap Index Scan on t_16_pkey  (cost=0.00..5932.64 rows=462857 width=0)
                    Index Cond: ((abs_date_time >= '2019-11-26 13:00:00'::timestamp without time zone) AND (abs_date_time < '2019-11-26 13:00:15'::timestamp without time zone))
_

UNION =高速

[〜#〜] union [〜#〜]条件を使用するようにクエリを書き換えると、高速になります。このクエリは、開発用PCで〜220msかかります:

_SELECT t_16_gen.*
FROM t_16_gen
WHERE (abs_date_time >= '2019-11-26 12:00:00' AND abs_date_time < '2019-11-26 12:00:15')
UNION
SELECT t_16_gen.*
FROM t_16_gen
WHERE (abs_date_time >= '2019-11-26 13:00:00' AND abs_date_time < '2019-11-26 13:00:15');
_

計画を説明する:

_Unique  (cost=1032439.64..1069468.20 rows=925714 width=80)
  ->  Sort  (cost=1032439.64..1034753.93 rows=925714 width=80)
"        Sort Key: t_16_gen.abs_date_time, t_16_gen.c_422, t_16_gen.c_423, t_16_gen.c_424, t_16_gen.c_425, t_16_gen.c_426, t_16_gen.c_427, t_16_gen.c_428, t_16_gen.c_429, t_16_gen.c_430, t_16_gen.c_431, t_16_gen.c_432, t_16_gen.c_433, t_16_gen.c_434, t_16_gen.c_435"
        ->  Append  (cost=0.57..892513.13 rows=925714 width=80)
              ->  Index Scan using t_16_pkey on t_16_gen  (cost=0.57..439313.71 rows=462857 width=80)
                    Index Cond: ((abs_date_time >= '2019-11-26 12:00:00'::timestamp without time zone) AND (abs_date_time < '2019-11-26 12:00:15'::timestamp without time zone))
              ->  Index Scan using t_16_pkey on t_16_gen t_16_gen_1  (cost=0.57..439313.71 rows=462857 width=80)
                    Index Cond: ((abs_date_time >= '2019-11-26 13:00:00'::timestamp without time zone) AND (abs_date_time < '2019-11-26 13:00:15'::timestamp without time zone))
_

問題を再現する

この問題を再現するために、新しいテーブルを作成し、ダミーデータで埋めることができます。次に、各テストの前にデータベースを再起動して、データがキャッシュされないようにします。
注:挿入クエリは数分間実行される場合があります!

_create table if not exists t_16_gen (
    abs_date_time timestamp constraint t_16_pkey primary key,
    c_422 bigint,
    c_423 bigint,
    c_424 real,
    c_425 real,
    c_426 real,
    c_427 real,
    c_428 real,
    c_429 real,
    c_430 bigint,
    c_431 real,
    c_432 real,
    c_433 real,
    c_434 bigint,
    c_435 real
);

INSERT INTO t_16_gen
SELECT ts, 1,2,3,4,5,6,7,8,9,10,11,12,13,14
FROM (SELECT generate_series('2019-11-26'::timestamp, '2019-11-27', '1 millisecond') as ts) as gs;
_
1
TmTron

最後の(高速)クエリには2つの同一のWHERE条件があり、Postgresはこれを識別してone。したがって、インデックス条件が1つだけの単純なプランになります。

複数の異なる条件を使用すると、より高価になります。しかし、Postgresは実際の入力値の推定に基づいて引き続き動作します。テーブルの大部分またはすべてを含むWHERE句で1つ以上の大きな間隔で試行すると、代わりに順次スキャンが表示されます。

VALUES式に基づく最初の2つのクエリでは、原則として異なります。そこで、Postgresは2つのケースをフォークします。

  • one入力行の場合、Postgresは実際の値を調べ、3番目のクエリと同じプランを生成します。実際の入力値に基づく推定値を持つ単一のWHERE条件。それに応じて、インデックス/ビットマップインデックス/順次スキャンを取得します。
  • 複数入力行の場合、Postgresは個々の値の参照を停止し、クエリプランに基づいて準備しますongeneric推定値と実際の入力行数。 VALUES式に5行を指定すると、結果がまったく返されないか、5行でテーブル全体が返されます。これは同じクエリプランになります。

Postgres 11でテスト済み。

また、セット(VALUES式)への結合は、複数のORの範囲述語を追加することとは論理的に異なることに注意してください。セット内の複数の時間範囲に一致する行は複数回返されますが、2番目の形式は、たとえそれが複数の述語に一致する場合でも、単一のインスタンスのみを返します。

そのため、多くのORを含む2番目のフォームは、当然、ビットマップインデックススキャンを優先します。これにより、複数のヒットが自動的に1つに折りたたまれます。 Postgresはあなたの状態が決して重ならないということを知りません。 (または、それらですか??次に、より大きな問題が発生します。)テーブル内のデータが物理的に時間順にソートされている場合(PK列abs_date_timeに一致)、これは引き続きうまく機能します偶然

しかし、行はかなり広い(ページあたりのタプルが少ない)ため、多くの時間範囲(最大200?)がある場合、ビットマップインデックススキャンを過度に優先すると、不利になる可能性があります。すべての単純なインデックススキャンが高速になる場合があります。

解決

UNION ALLsuperiorである必要があります!

SELECT * FROM s_28.t_16 WHERE abs_date_time >= '2019-11-26 12:00:00' AND abs_date_time < '2019-11-26 12:10:00'
UNION ALL
SELECT * FROM s_28.t_16 WHERE abs_date_time >= '2019-11-26 13:00:00' AND abs_date_time < '2019-11-26 13:10:00'
-- add (many) more
;

まず第一に、これはここで機能するロジックに最適です。 Postgresの将来のバージョンが優れたクエリプランを使い続ける可能性ははるかに優れています。

このように、Postgresは各SELECTの実際の入力に基づいて推定値を使用し、仕様を指定すると(すべての範囲が小さい)、クエリはneverテーブル統計が完全に誤解を招かない限り、順次スキャンに低下します。

また、インデックススキャンは、ビットマップインデックススキャンに対して(不公平な)不利な点ではなくなりました。

2