スキーマ:
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
インデックスは使用されていません。
ノート:
btree_Gist
拡張機能がインストールされています。created_at timestamp without time zone
で同等の設定を試し、tsrange
を使用しました。同じ結果。>=
、<
チェックを使用してクエリを書き換えると、btreeインデックスが使用されることを理解しています。問題は、tstzrange
包含演算子でインデックスが使用されない理由と、それを機能させる方法があるかどうかです。私の研究に関する限り、 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
_制約をチェックする理由は、最初に範囲スキャンから利益を得て、次に結果をフィルタリングするためです。
問題は、
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))
_