Aangepast 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();
Belangrijkste punten
-
Gebruik
. Lieverchar(n)
nietvarchar(n)
, of beter nog,varchar
of gewoontext
. -
Gebruik niet de naam van een worker als primaire sleutel. Het is niet per se uniek en kan veranderen. Gebruik in plaats daarvan een surrogaat primaire sleutel, het beste een
serial
. Maakt ook vermeldingen inreservat
kleiner, indexen kleiner, zoekopdrachten sneller, ... -
Bijwerken: Voor goedkopere opslag (8 bytes in plaats van 22) en eenvoudigere bediening sla ik begin en einde op als
time
nu en maak direct een bereik voor de uitsluitingsbeperking:EXCLUDE USING gist (worker_id WITH =, day WITH = , timerange(work_from, work_to) WITH &&)
-
Aangezien uw bereiken nooit de datumgrens kunnen overschrijden per definitie zou het efficiënter zijn om een aparte
date
te hebben kolom (day
in mijn implementatie) en een tijdbereik . Het typetimerange
wordt niet geleverd in standaardinstallaties, maar is eenvoudig te maken. Op deze manier kunt u uw controlebeperkingen grotendeels vereenvoudigen. -
Gebruik
EXTRACT('isodow', ...)
om de zondagen te vereenvoudigen -
Ik neem aan dat je wilt toestaan de bovenrand van '21:00'.
-
Grenzen worden geacht inclusief te zijn voor de ondergrens en exclusief voor de bovengrens.
-
De controle of nieuwe / bijgewerkte dagen binnen een maand vanaf "nu" liggen, is niet
IMMUTABLE
. Verplaatst van deCHECK
beperking van de trigger - anders kunt u problemen krijgen met dump / restore! Details:
Terzijde
Naast het vereenvoudigen van invoer- en controlebeperkingen verwachtte ik timerange
om 8 bytes opslagruimte te besparen in vergelijking met tsrange
sinds time
neemt slechts 4 bytes in beslag. Maar het blijkt timerange
neemt 22 bytes in beslag op schijf (25 in RAM), net als tsrange
(of tstzrange
). Dus je zou kunnen gaan met tsrange
ook. Het principe van query en uitsluitingsbeperking zijn hetzelfde.
Zoekopdracht
Verpakt in een SQL-functie voor gemakkelijke parameterafhandeling:
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;
Bel:
SELECT * FROM f_next_free('2014-10-28 12:00'::timestamp, '1.5 h'::interval);
SQL Fiddle nu op Postgres 9.3.
Uitleggen
-
De functie heeft een
_start
timestamp
als minimale starttijd en_duration interval
. Zorg ervoor dat u alleen eerdere tijden uitsluit op de start dag, niet de volgende dagen. Eenvoudigst door dag en tijd toe te voegen:t + d > _start
.
Als je een reservering wilt boeken die "nu" begint, geef je gewoonnow()::timestamp
door :SELECT * FROM f_next_free(`now()::timestamp`, '1.5 h'::interval);
-
Subquery
d
genereert dagen vanaf de invoerwaarde_day
. Feestdagen uitgesloten. - Dagen worden gekoppeld aan mogelijke tijdsperioden die zijn gegenereerd in subquery
t
. - Dat is gekoppeld aan alle beschikbare werkers
w
. - Eindelijk alle kandidaten elimineren die botsen met bestaande reserveringen met een
NOT EXISTS
anti-semi-join, en in het bijzonder de overlappingsoperator&&
.
Gerelateerd:
- Hoe reken je datums uit waarbij het jaartal wordt genegeerd? (voor rekenvoorbeeld van datum)
- Aangrenzende /overlappende items met EXCLUDE in PostgreSQL
- Bereken werken uur tussen 2 datums in PostgreSQL