テーブルレイアウト
テーブルを再設計して、営業時間(営業時間)を tsrange
のセットとして保存します。 (timestamp without time zone
の範囲 ) 値。 Postgres9.2以降が必要です 。
ランダムな週を選んで、営業時間をステージングします。私はその週が好きです:
1996-01-01(月曜日) 〜 1996-01-07(日曜日)
これは、1月1日が月曜日であることが都合のよい最近のうるう年です。ただし、この場合は任意の週になる可能性があります。一貫性を保つだけです。
追加モジュールbtree_gist
をインストールします 最初:
CREATE EXTENSION btree_gist;
参照:
- 整数と範囲で構成される除外制約に相当します
次に、次のようなテーブルを作成します。
CREATE TABLE hoo (
hoo_id serial PRIMARY KEY
, shop_id int NOT NULL -- REFERENCES shop(shop_id) -- reference to shop
, hours tsrange NOT NULL
, CONSTRAINT hoo_no_overlap EXCLUDE USING gist (shop_id with =, hours WITH &&)
, CONSTRAINT hoo_bounds_inclusive CHECK (lower_inc(hours) AND upper_inc(hours))
, CONSTRAINT hoo_standard_week CHECK (hours <@ tsrange '[1996-01-01 0:0, 1996-01-08 0:0]')
);
1つ 列hours
すべての列を置き換えます:
opens_on, closes_on, opens_at, closes_at
たとえば、水曜日の18:30からの営業時間 木曜日05:00 UTCは次のように入力されます:
'[1996-01-03 18:30, 1996-01-04 05:00]'
除外制約hoo_no_overlap
ショップごとのエントリの重複を防ぎます。 GiSTインデックスで実装されています 、これもクエリをサポートします。 「インデックスとパフォーマンス」の章を検討してください。 以下でインデックス作成戦略について説明します。
チェック制約hoo_bounds_inclusive
範囲に包括的境界を適用し、2つの注目すべき結果をもたらします:
- 下限または上限に正確に該当する時点が常に含まれます。
- 同じショップへの隣接するエントリは事実上禁止されています。包括的境界を使用すると、それらは「重複」し、除外制約によって例外が発生します。代わりに、隣接するエントリを1つの行にマージする必要があります。 日曜日の深夜にラップアラウンドする場合を除く 、この場合、2つの行に分割する必要があります。関数
f_hoo_hours()
以下がこれを処理します。
チェック制約hoo_standard_week
「範囲は含まれています」演算子<@
を使用して、ステージング週の外側の境界を強制します 。
包括的 境界、コーナーケースを観察する必要があります 日曜日の深夜に時間がラップアラウンドする場所:
'1996-01-01 00:00+0' = '1996-01-08 00:00+0'
Mon 00:00 = Sun 24:00 (= next Mon 00:00)
両方のタイムスタンプを一度に検索する必要があります。これは、排他的に関連するケースです。 この欠点を示さない上限:
- PostgreSQLでEXCLUDEを使用して隣接/重複するエントリを防止する
関数f_hoo_time(timestamptz)
任意のtimestamp with time zone
:
CREATE OR REPLACE FUNCTION f_hoo_time(timestamptz)
RETURNS timestamp
LANGUAGE sql IMMUTABLE PARALLEL SAFE AS
$func$
SELECT timestamp '1996-01-01' + ($1 AT TIME ZONE 'UTC' - date_trunc('week', $1 AT TIME ZONE 'UTC'))
$func$;
PARALLEL SAFE
Postgres9.6以降のみ。
この関数はtimestamptz
を取ります timestamp
を返します 。それぞれの週の経過間隔を追加します($1 - date_trunc('week', $1)
UTC時間で、ステージング週の開始点まで。 (date
+interval
timestamp
を生成します 。)
関数f_hoo_hours(timestamptz, timestamptz)
範囲を正規化し、月曜日00:00を通過する範囲を分割します。この関数は任意の間隔を取ります(2つのtimestamptz
として) )そして、1つまたは2つの正規化されたtsrange
を生成します 値。 すべてをカバーします 法的な入力と残りの禁止:
CREATE OR REPLACE FUNCTION f_hoo_hours(_from timestamptz, _to timestamptz)
RETURNS TABLE (hoo_hours tsrange)
LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE COST 500 ROWS 1 AS
$func$
DECLARE
ts_from timestamp := f_hoo_time(_from);
ts_to timestamp := f_hoo_time(_to);
BEGIN
-- sanity checks (optional)
IF _to <= _from THEN
RAISE EXCEPTION '%', '_to must be later than _from!';
ELSIF _to > _from + interval '1 week' THEN
RAISE EXCEPTION '%', 'Interval cannot span more than a week!';
END IF;
IF ts_from > ts_to THEN -- split range at Mon 00:00
RETURN QUERY
VALUES (tsrange('1996-01-01', ts_to , '[]'))
, (tsrange(ts_from, '1996-01-08', '[]'));
ELSE -- simple case: range in standard week
hoo_hours := tsrange(ts_from, ts_to, '[]');
RETURN NEXT;
END IF;
RETURN;
END
$func$;
INSERT
へ シングル 入力行:
INSERT INTO hoo(shop_id, hours)
SELECT 123, f_hoo_hours('2016-01-11 00:00+04', '2016-01-11 08:00+04');
任意の場合 入力行数:
INSERT INTO hoo(shop_id, hours)
SELECT id, f_hoo_hours(f, t)
FROM (
VALUES (7, timestamptz '2016-01-11 00:00+0', timestamptz '2016-01-11 08:00+0')
, (8, '2016-01-11 00:00+1', '2016-01-11 08:00+1')
) t(id, f, t);
月曜日00:00UTCに範囲を分割する必要がある場合は、それぞれに2つの行を挿入できます。
クエリ
調整されたデザインで、大きくて複雑で高価なクエリ全体 ... this:
に置き換えることができます
SELECT *
FROM hoo
WHERE hours @> f_hoo_time(now());
少しサスペンドするために、溶液の上にスポイラープレートを置きます。 マウスを上に移動 それ。
クエリは、GiSTインデックスに裏打ちされており、大きなテーブルの場合でも高速です。
db<>ここでフィドル (その他の例を含む)
古いsqlfiddle
総営業時間(ショップあたり)を計算する場合は、レシピを次に示します。
- PostgreSQLで2つの日付の間の労働時間を計算します
インデックスとパフォーマンス
範囲タイプの包含演算子は、GiSTまたは SP-GiSTでサポートできます。 索引。どちらも除外制約を実装するために使用できますが、GiSTのみが複数列のインデックスをサポートします:
現在、マルチカラムインデックスをサポートしているのは、Bツリー、GiST、GIN、およびBRINインデックスタイプのみです。
そして、インデックス列の順序は重要です:
複数列のGiSTインデックスは、インデックスの列のサブセットを含むクエリ条件で使用できます。追加の列の条件は、インデックスによって返されるエントリを制限しますが、最初の列の条件は、スキャンする必要のあるインデックスの量を決定するための最も重要な条件です。追加の列に多数の個別の値がある場合でも、最初の列に少数の個別の値しかない場合、GiSTインデックスは比較的効果がありません。
したがって、相反する利益があります ここ。大きなテーブルの場合、shop_id
にはさらに多くの異なる値があります hours
より 。
- 先頭に
shop_id
が付いたGiSTインデックス 書き込みと除外制約の適用が高速です。 - しかし、私たちは
hours
を検索しています 私たちのクエリで。その列を最初に持つ方が良いでしょう。 -
shop_id
を検索する必要がある場合 他のクエリでは、プレーンなbtreeインデックスの方がはるかに高速です。 - 最後に、 SP-GiSTを見つけました。 ちょうど
hours
のインデックス 最速になる クエリ用。
ベンチマーク
古いラップトップでのPostgres12を使用した新しいテスト。ダミーデータを生成するためのスクリプト:
INSERT INTO hoo(shop_id, hours)
SELECT id
, f_hoo_hours(((date '1996-01-01' + d) + interval '4h' + interval '15 min' * trunc(32 * random())) AT TIME ZONE 'UTC'
, ((date '1996-01-01' + d) + interval '12h' + interval '15 min' * trunc(64 * random() * random())) AT TIME ZONE 'UTC')
FROM generate_series(1, 30000) id
JOIN generate_series(0, 6) d ON random() > .33;
結果として、ランダムに生成された行が約141k、個別のshop_id
が約30kになります。 、〜12k個の異なるhours
。テーブルサイズ8MB。
除外制約を削除して再作成しました:
ALTER TABLE hoo
DROP CONSTRAINT hoo_no_overlap
, ADD CONSTRAINT hoo_no_overlap EXCLUDE USING gist (shop_id WITH =, hours WITH &&); -- 3.5 sec; index 8 MB
ALTER TABLE hoo
DROP CONSTRAINT hoo_no_overlap
, ADD CONSTRAINT hoo_no_overlap EXCLUDE USING gist (hours WITH &&, shop_id WITH =); -- 13.6 sec; index 12 MB
shop_id
1つ目は、このディストリビューションでは約4倍高速です。
さらに、読み取りパフォーマンスについてさらに2つテストしました。
CREATE INDEX hoo_hours_gist_idx on hoo USING gist (hours);
CREATE INDEX hoo_hours_spgist_idx on hoo USING spgist (hours); -- !!
VACUUM FULL ANALYZE hoo;
の後 、2つのクエリを実行しました:
- Q1 :深夜、35行のみを検索
- Q2 :午後、4547行を見つける 。
結果
インデックスのみのスキャンを取得しました それぞれについて(もちろん「インデックスなし」を除く):
index idx size Q1 Q2
------------------------------------------------
no index 38.5 ms 38.5 ms
gist (shop_id, hours) 8MB 17.5 ms 18.4 ms
gist (hours, shop_id) 12MB 0.6 ms 3.4 ms
gist (hours) 11MB 0.3 ms 3.1 ms
spgist (hours) 9MB 0.7 ms 1.8 ms -- !
- SP-GiSTとGiSTは、結果がほとんどないクエリと同等です(GiSTは非常に 少数)。
- SP-GiSTは、結果の数が増えるにつれてスケーリングが向上し、サイズも小さくなります。
書くよりも多く読む場合(通常のユースケース)、最初に提案されたように除外制約を維持し、読み取りパフォーマンスを最適化するために追加のSP-GiSTインデックスを作成します。