Angepasstes 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();
Wichtige Punkte
-
Verwenden Sie
. Eherchar(n)
nichtvarchar(n)
, oder noch besser,varchar
oder nurtext
. -
Verwenden Sie nicht den Namen eines Arbeiters als Primärschlüssel. Es ist nicht unbedingt einzigartig und kann sich ändern. Verwenden Sie stattdessen einen Ersatz-Primärschlüssel, am besten eine
serial
. Macht auch Einträge inreservat
kleiner, Indizes kleiner, Abfragen schneller, ... -
Aktualisierung: Für günstigeren Speicherplatz (8 Bytes statt 22) und einfachere Handhabung speichere ich Start und Ende als
time
Jetzt und konstruieren Sie spontan einen Bereich für die Ausschlussbeschränkung:EXCLUDE USING gist (worker_id WITH =, day WITH = , timerange(work_from, work_to) WITH &&)
-
Da Ihre Bereiche niemals die Datumsgrenze überschreiten können Definitionsgemäß wäre es effizienter, ein separates
date
zu haben Spalte (day
in meiner Implementierung) und einen Zeitbereich . Der Typtimerange
wird nicht in Standardinstallationen mitgeliefert, aber einfach erstellt. Auf diese Weise können Sie Ihre Check Constraints stark vereinfachen. -
Verwenden Sie
EXTRACT('isodow', ...)
zur Vereinfachung ohne Sonntage -
Ich nehme an, Sie möchten erlauben die obere Grenze von '21:00'.
-
Es wird davon ausgegangen, dass Grenzen für die Untergrenze einschließend und für die Obergrenze ausschließend sind.
-
Die Prüfung, ob neue / aktualisierte Tage innerhalb eines Monats von "jetzt" liegen, ist nicht
IMMUTABLE
. Aus demCHECK
verschoben Einschränkung auf den Trigger - sonst könnten Sie Probleme mit Dump / Restore bekommen! Einzelheiten:
Beiseite
Neben der Vereinfachung von Eingabe- und Prüfungsbeschränkungen erwartete ich timerange
um 8 Byte Speicherplatz im Vergleich zu tsrange
zu sparen seit time
belegt nur 4 Bytes. Aber es stellt sich heraus timerange
belegt 22 Bytes auf der Festplatte (25 im RAM), genau wie tsrange
(oder tstzrange
). Sie könnten also mit tsrange
gehen auch. Das Prinzip der Abfrage- und Ausschlussbeschränkung ist dasselbe.
Abfrage
Eingebettet in eine SQL-Funktion zur bequemen Parameterbehandlung:
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;
Aufruf:
SELECT * FROM f_next_free('2014-10-28 12:00'::timestamp, '1.5 h'::interval);
SQL-Fiddle jetzt auf Postgres 9.3.
Erklären
-
Die Funktion benötigt einen
_start
timestamp
als minimale Startzeit und_duration interval
. Achten Sie darauf, nur frühere Zeiten beim Start auszuschließen Tag, nicht die folgenden Tage. Am einfachsten durch Hinzufügen von Tag und Uhrzeit:t + d > _start
.
Um eine Reservierung ab "jetzt" zu buchen, übergeben Sie einfachnow()::timestamp
:SELECT * FROM f_next_free(`now()::timestamp`, '1.5 h'::interval);
-
Unterabfrage
d
generiert Tage ab dem Eingabewert_day
. Feiertage ausgeschlossen. - Tage werden mit möglichen Zeitbereichen verknüpft, die in der Unterabfrage
t
generiert werden . - Das ist Cross-Joined mit allen verfügbaren Workern
w
. - Entfernen Sie schließlich alle Kandidaten, die mit bestehenden Reservierungen kollidieren, indem Sie einen
NOT EXISTS
verwenden Anti-Semi-Join und insbesondere der Overlaps-Operator&&
.
Verwandte:
- Wie macht man Datumsberechnungen, bei denen das Jahr ignoriert wird? (für Datumsmathe-Beispiel)
- Benachbarte verhindern /Überlappende Einträge mit EXCLUDE in PostgreSQL
- Arbeiten berechnen Stunden zwischen 2 Daten in PostgreSQL