web-dev-qa-db-ja.com

包含チェックtstzrange @> timestamptzはbtreeまたはGistインデックスを使用していません

スキーマ:

        Column        |           Type           
----------------------+--------------------------
 id                   | integer                  
 event_id             | integer                  
 started_at           | timestamp with time zone 
 ended_at             | timestamp with time zone 
 created_at           | timestamp with time zone 
    "event_seat_state_lookup_pkey" PRIMARY KEY, btree (id)
    "event_seating_lookup_created_at_idx" btree (created_at)
    "event_seating_lookup_created_at_idx2" Gist (created_at)

クエリ:

SELECT id
FROM event_seating_lookup esl1
WHERE
  tstzrange(now() - interval '1 hour', now() + interval '1 hour', '[)') @> esl1.created_at;

分析の説明:

10万行未満のテーブル。

 Seq Scan on event_seating_lookup esl1  (cost=0.00..1550.30 rows=148 width=4) (actual time=0.013..19.956 rows=29103 loops=1)
   Filter: (tstzrange((now() - '01:00:00'::interval), (now() + '01:00:00'::interval), '[)'::text) @> created_at)
   Buffers: shared hit=809
 Planning Time: 0.110 ms
 Execution Time: 21.942 ms

100万行以上の表:

Seq Scan on event_seating_lookup esl1  (cost=10000000000.00..10000042152.75 rows=5832 width=4) (actual time=0.009..621.895 rows=1166413 loops=1)
  Filter: (tstzrange((now() - '01:00:00'::interval), (now() + '01:00:00'::interval), '[)'::text) @> created_at)
  Buffers: shared hit=12995
Planning Time: 0.092 ms
Execution Time: 697.927 ms

私が試してみました:

VACUUM FULL event_seating_lookup;
VACUUM event_seating_lookup;
VACUUM ANALYZE event_seating_lookup;
SET enable_seqscan = OFF;

問題:

event_seating_lookup_created_at_idxまたはevent_seating_lookup_created_at_idx2インデックスは使用されていません。

ノート:

  • PostgreSQL 11.1.
  • btree_Gist拡張機能がインストールされています。
  • created_at timestamp without time zoneで同等の設定を試し、tsrangeを使用しました。同じ結果。
  • >=<チェックを使用してクエリを書き換えると、btreeインデックスが使用されることを理解しています。問題は、tstzrange包含演算子でインデックスが使用されない理由と、それを機能させる方法があるかどうかです。
2
Gajus

私の研究に関する限り、 postgresql は、包含チェックをbtreeインデックスを使用して一致する可能性のある式に書き換えることができません。

_esl1.created_at >= now() - interval '1 hour' AND
esl1.created_at < now() + interval '1 hour'
_

このように記述した場合、クエリはインデックスを使用して実行されます。

_Index Scan using event_seating_lookup_created_at_idx on event_seating_lookup esl1  (cost=0.44..12623.56 rows=18084 width=4) (actual time=0.013..57.084 rows=70149 loops=1)
  Index Cond: ((created_at >= (now() - '01:00:00'::interval)) AND (created_at < (now() + '01:00:00'::interval)))
Planning Time: 0.223 ms
Execution Time: 62.209 ms
_

後者のフォームよりも包含クエリの構文を好むので、可能な代替案を調査しました。思いついたのは、条件をインライン化するプロシージャを記述できることです。

_CREATE OR REPLACE FUNCTION in_range(timestamptz, tstzrange)
RETURNS boolean AS $$
SELECT
  (
    $1 >= lower($2) AND
    $1 <= upper($2) AND
    (
      upper_inc($2) OR
      $1 < upper($2)
    ) AND
    (
      lower_inc($2) OR
      $1 > lower($2)
    )
  )
$$
language sql;
_

$1 >= lower($2) AND $1 <= upper($2)条件で最初に一致し、次に_upper_inc_および_lower_inc_制約をチェックする理由は、最初に範囲スキャンから利益を得て、次に結果をフィルタリングするためです。

2
Gajus

問題は、tstzrange包含演算子でインデックスが使用されない理由と、それを機能させる方法があるかどうかです。

reasonは非常に簡単です。 Bツリーインデックスは、包含演算子_@>_をサポートしていません。 tstzrange のような範囲型の場合も、他の型(配列型を含む)の場合もありません。

マニュアル:

... btree演算子クラスは、_<_、_<=_、_=_、_>=_および_>_の5つの比較演算子を提供する必要があります。

また、Gistインデックスは_<_、_<=_、> _=_、_>=_および_>_をサポートしていません。マニュアルの次の章を参照してください。

Postgresでは、インデックスはデータ型のみや関数などではなく、演算子(特定の型に実装されている)にバインドされています。関連:

Gistインデックス_event_seating_lookup_created_at_idx2_はpointlessです。 timestamptz列_created_at_に作成されます。このようなGistインデックスは、rangeタイプ(ロジックの反対方向)に役立ちます。

timestamptz列にGistインデックスを作成することは、そのような役に立たないインデックスを許可するために追加の_btree_Gist_拡張機能をインストールしたためにのみ可能です。 (複数列のインデックスや除外制約に役立つアプリケーションがあります...)在庫のあるPostgresではエラーが発生します。

エラー:タイムゾーン付きのデータ型タイムスタンプには、アクセス方法「Gist」のデフォルトの演算子クラスがありません

したがって、クエリにbtreeインデックス(またはGistインデックス)を使用することは論理的には有効で技術的に可能ですが、ケースは実装されていません:no index support for _timestamptz <@ tstzrange_(where timestamptzはインデックス付きの式になります!)。 _<_、_<=_、_>_、_>=_でより効率的に解決できます。そのため、開発者はそれを実装する必要性を感じなかった(または感じるだろう)と思います。

好都合な演算子で式を書き換える機能

あなたの実装 は私には理にかなっています。 function inlining により、タイムスタンプ列のプレーンなbtreeインデックス(例では_event_seating_lookup_created_at_idx_)を使用しています。 定数範囲の呼び出し(単一の関数呼び出しのような)の場合、この変更されたバージョンをお勧めします。

_CREATE OR REPLACE FUNCTION in_range(timestamptz, tstzrange)
  RETURNS boolean AS
$func$
SELECT CASE WHEN lower_inc($2) THEN $1 >= lower($2) ELSE $1 > lower($2) END
   AND CASE WHEN upper_inc($2) THEN $1 <= upper($2) ELSE $1 < upper($2) END
$func$  LANGUAGE sql IMMUTABLE;
_

実際にそうなので、それをIMMUTABLEと宣言します。関数のインライン化を支援せず(宣言がfalseの場合にそれを防止することもできます)、他の利点があります。関連:

インライン化でき、バージョンと同じようにインデックスを使用します。違い:これは、排他的境界の冗長なインデックス条件を抑制します。幸いなことに、この点についての考慮は目標からわずかに外れています。

the $1 >= lower($2) AND $1 <= upper($2)条件で最初に一致し、次に_upper_inc_および_lower_inc_制約をチェックする理由は、最初に範囲スキャンを実行してから結果をフィルター処理するためです。

Postgres 11は(少なくとも)それよりもさらに賢いです。お使いのバージョンにはFilterステップがありません。デフォルトの_[)_境界(例のように)の場合、このクエリプランを取得します(自分で追加した条件の改行):

_  ->  Index Only Scan using foo_idx on foo (actual rows=5206 loops=1)
        Index Cond: ((datetime >= '2018-09-05 22:00:00+00'::timestamp with time zone)
                 AND (datetime <= '2018-09-05 22:30:00+00'::timestamp with time zone)
                 AND (datetime < '2018-09-05 22:30:00+00'::timestamp with time zone))
_

実際のFilterステップは、除外された境界に多くのヒットがあるコーナーケースに、より大きなコストを追加する可能性があります。それらはインデックスからフェッチされてから破棄されます。 1時間のタイムスタンプのように、値がしばしば境界に達する時間範囲にかなり関連しています。

実際の違いはわずかですが、取りませんか?

_  ->  Index Only Scan using foo_idx on foo (actual rows=5206 loops=1)
        Index Cond: ((datetime >= '2018-09-05 22:00:00+00'::timestamp with time zone)
                 AND (datetime < '2018-09-05 22:30:00+00'::timestamp with time zone))
_
2