適応スキーマ
CREATE EXTENSION btree_gist;
CREATE TYPE timerange AS RANGE (subtype = time); -- create type once
-- Workers
CREATE TABLE worker(
worker_id serial PRIMARY KEY
, worker text NOT NULL
);
INSERT INTO worker(worker) VALUES ('JOHN'), ('MARY');
-- Holidays
CREATE TABLE pyha(pyha date PRIMARY KEY);
-- Reservations
CREATE TABLE reservat (
reservat_id serial PRIMARY KEY
, worker_id int NOT NULL REFERENCES worker ON UPDATE CASCADE
, day date NOT NULL CHECK (EXTRACT('isodow' FROM day) < 7)
, work_from time NOT NULL -- including lower bound
, work_to time NOT NULL -- excluding upper bound
, CHECK (work_from >= '10:00' AND work_to <= '21:00'
AND work_to - work_from BETWEEN interval '15 min' AND interval '4 h'
AND EXTRACT('minute' FROM work_from) IN (0, 15, 30, 45)
AND EXTRACT('minute' FROM work_from) IN (0, 15, 30, 45)
)
, EXCLUDE USING gist (worker_id WITH =, day WITH =
, timerange(work_from, work_to) WITH &&)
);
INSERT INTO reservat (worker_id, day, work_from, work_to) VALUES
(1, '2014-10-28', '10:00', '11:30') -- JOHN
, (2, '2014-10-28', '11:30', '13:00'); -- MARY
-- Trigger for volatile checks
CREATE OR REPLACE FUNCTION holiday_check()
RETURNS trigger AS
$func$
BEGIN
IF EXISTS (SELECT 1 FROM pyha WHERE pyha = NEW.day) THEN
RAISE EXCEPTION 'public holiday: %', NEW.day;
ELSIF NEW.day < now()::date OR NEW.day > now()::date + 31 THEN
RAISE EXCEPTION 'day out of range: %', NEW.day;
END IF;
RETURN NEW;
END
$func$ LANGUAGE plpgsql STABLE; -- can be "STABLE"
CREATE TRIGGER insupbef_holiday_check
BEFORE INSERT OR UPDATE ON reservat
FOR EACH ROW EXECUTE PROCEDURE holiday_check();
主なポイント
-
char(n)
は使用しないでください ストライク> 。むしろvarchar(n)
、またはさらに良いことに、varchar
または単にtext
。 -
ワーカーの名前を主キーとして使用しないでください。必ずしも一意である必要はなく、変更される可能性があります。代わりに代理主キーを使用してください。
serial
が最適です。 。reservat
にもエントリを作成します 小さい、インデックスが小さい、クエリが速い、... -
更新: より安価なストレージ(22ではなく8バイト)とより簡単な処理のために、開始と終了を
time
として保存します。 ここで、除外制約の範囲をその場で作成します。EXCLUDE USING gist (worker_id WITH =, day WITH = , timerange(work_from, work_to) WITH &&)
-
範囲は日付の境界を越えることはできませんので 定義上、個別の
date
を使用する方が効率的です。 列(day
私の実装では)および時間範囲 。 タイプtimerange
デフォルトのインストールでは出荷されませんが、簡単に作成できます。 このようにして、チェック制約を大幅に簡素化できます。 -
許可したいと思います 「21:00」の上枠。
-
境界線は、下限を含み、上限を除外すると想定されます。
-
新しい/更新された日が「今」から1か月以内にあるかどうかのチェックは、
IMMUTABLE
ではありません。 。CHECK
から移動しました トリガーへの制約-そうしないと、ダンプ/復元で問題が発生する可能性があります!詳細:
脇
入力とチェック制約を単純化することに加えて、私はtimerange
を期待していました tsrange
と比較して8バイトのストレージを節約します time
以降 4バイトしか占有しません。しかし、timerange
であることが判明しました tsrange
と同じように、ディスク上で22バイト(RAM内で25バイト)を占有します。 (またはtstzrange
)。したがって、tsrange
を使用する場合があります 同じように。クエリと除外の制約の原則は同じです。
クエリ
便利なパラメータ処理のためにSQL関数にラップされています:
CREATE OR REPLACE FUNCTION f_next_free(_start timestamp, _duration interval)
RETURNS TABLE (worker_id int, worker text, day date
, start_time time, end_time time) AS
$func$
SELECT w.worker_id, w.worker
, d.d AS day
, t.t AS start_time
,(t.t + _duration) AS end_time
FROM (
SELECT _start::date + i AS d
FROM generate_series(0, 31) i
LEFT JOIN pyha p ON p.pyha = _start::date + i
WHERE p.pyha IS NULL -- eliminate holidays
) d
CROSS JOIN (
SELECT t::time
FROM generate_series (timestamp '2000-1-1 10:00'
, timestamp '2000-1-1 21:00' - _duration
, interval '15 min') t
) t -- times
CROSS JOIN worker w
WHERE d.d + t.t > _start -- rule out past timestamps
AND NOT EXISTS (
SELECT 1
FROM reservat r
WHERE r.worker_id = w.worker_id
AND r.day = d.d
AND timerange(r.work_from, r.work_to) && timerange(t.t, t.t + _duration)
)
ORDER BY d.d, t.t, w.worker, w.worker_id
LIMIT 30 -- could also be parameterized
$func$ LANGUAGE sql STABLE;
電話:
SELECT * FROM f_next_free('2014-10-28 12:00'::timestamp, '1.5 h'::interval);
SQLフィドル Postgres9.3で今すぐ。
説明
-
この関数は
_start
を取りますtimestamp
最小開始時間および_duration interval
として 。 開始の早い時間のみを除外するように注意してください 翌日ではなく、日。日時を追加するだけで最も簡単:t + d > _start
。
「今」から始まる予約を予約するには、now()::timestamp
を渡すだけです。 :SELECT * FROM f_next_free(`now()::timestamp`, '1.5 h'::interval);
-
サブクエリ
d
入力値_day
から始まる日数を生成します 。休日は除きます。 - 日は、サブクエリ
t
で生成される可能な時間範囲と相互結合されます 。 - これは、利用可能なすべてのワーカーに相互結合されます
w
。 - 最後に、
NOT EXISTS
を使用して、既存の予約と衝突するすべての候補を削除します 反半結合、特にオーバーラップ演算子&&
。
関連: