Anpassat schema
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();
Huvudpunkter
-
Använd inte
. Snararechar(n)varchar(n), eller ännu bättre,varchareller baratext. -
Använd inte namnet på en arbetare som primärnyckel. Det är inte nödvändigtvis unikt och kan förändras. Använd en surrogatprimärnyckel istället, bäst en
serial. Gör även inlägg ireservatmindre, indexerar mindre, sökningar snabbare, ... -
Uppdatering: För billigare lagring (8 byte istället för 22) och enklare hantering sparar jag start och slut som
timenu och konstruera ett intervall i farten för uteslutningsrestriktionen:EXCLUDE USING gist (worker_id WITH =, day WITH = , timerange(work_from, work_to) WITH &&) -
Eftersom dina intervall aldrig kan passera datumgränsen per definition skulle det vara mer effektivt att ha ett separat
datekolumn (dayi min implementering) och ett tidsintervall . Typentimerangelevereras inte i standardinstallationer, men skapas enkelt. På så sätt kan du till stor del förenkla dina checkbegränsningar. -
Använd
EXTRACT('isodow', ...)för att förenkla att utesluta söndagar -
Jag antar att du vill tillåta den övre kanten på '21:00'.
-
Gränser antas vara inklusive för den nedre och exklusive för den övre gränsen.
-
Kontrollen om nya / uppdaterade dagar ligger inom en månad från "nu" är inte
IMMUTABLE. Flyttade den frånCHECKbegränsning för utlösaren - annars kan du stöta på problem med dumpning/återställning! Detaljer:
Åsido
Förutom att förenkla inmatnings- och kontrollbegränsningar förväntade jag mig timerange för att spara 8 byte lagring jämfört med tsrange sedan time upptar endast 4 byte. Men det visar sig timerange upptar 22 byte på disken (25 i RAM-minne), precis som tsrange (eller tstzrange ). Så du kan välja tsrange också. Principen för fråge- och uteslutningsbegränsning är densamma.
Fråga
Inlindad i en SQL-funktion för bekväm parameterhantering:
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;
Ring:
SELECT * FROM f_next_free('2014-10-28 12:00'::timestamp, '1.5 h'::interval);
SQL Fiddle på Postgres 9.3 nu.
Förklara
-
Funktionen tar en
_starttimestampsom minsta starttid och_duration interval. Var noga med att endast utesluta tidigare tider vid start dag, inte följande dagar. Enklast genom att bara lägga till dag och tid:t + d > _start.
För att boka en reservation som börjar "nu", skicka baranow()::timestamp:SELECT * FROM f_next_free(`now()::timestamp`, '1.5 h'::interval); -
Underfråga
dgenererar dagar från ingångsvärdet_day. Helgdagar undantagna. - Dagarna är sammanfogade med möjliga tidsintervall som genereras i underfrågan
t. - Detta är korskopplat till alla tillgängliga arbetare
w. - Äntligen eliminera alla kandidater som kolliderar med befintliga reservationer med en
NOT EXISTSanti-semi-join, och i synnerhet överlappningsoperatorn&&.
Relaterat:
- Hur gör du dejtmatematik som ignorerar årtalet? (för datummatteexempel)
- Förhindra intilliggande /överlappande poster med EXCLUDE i PostgreSQL
- Räkna arbete timmar mellan 2 datum i PostgreSQL