Schemat dostosowany
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();
Główne punkty
-
Nie używaj
. Raczejchar(n)
varchar(n)
lub jeszcze lepiejvarchar
lub po prostutext
. -
Nie używaj nazwy pracownika jako klucza podstawowego. Niekoniecznie jest wyjątkowy i może się zmieniać. Zamiast tego użyj zastępczego klucza podstawowego, najlepiej
serial
. Tworzy również wpisy wreservat
mniejsze, indeksy mniejsze, zapytania szybsze, ... -
Aktualizacja: Dla tańszego przechowywania (8 bajtów zamiast 22) i prostszej obsługi zapisuję początek i koniec jako
time
teraz i skonstruuj w locie zakres dla ograniczenia wykluczenia:EXCLUDE USING gist (worker_id WITH =, day WITH = , timerange(work_from, work_to) WITH &&)
-
Ponieważ Twoje zakresy nigdy nie przekroczą granicy dat z definicji bardziej efektywne byłoby posiadanie oddzielnej
date
kolumna (day
w mojej implementacji) i zakres czasu . Typtimerange
nie jest dostarczany w domyślnych instalacjach, ale można go łatwo utworzyć. W ten sposób możesz w dużym stopniu uprościć ograniczenia sprawdzania. -
Zakładam, że chcesz zezwolić górna granica „21:00”.
-
Zakłada się, że granice obejmują dolną granicę i wykluczają górną granicę.
-
Sprawdzenie, czy nowe / zaktualizowane dni leżą w ciągu miesiąca od „teraz”, nie jest
IMMUTABLE
. Przeniesiono go zCHECK
ograniczenie do wyzwalacza - w przeciwnym razie możesz napotkać problemy ze zrzutem / przywróceniem! Szczegóły:
Na bok
Oprócz uproszczenia wprowadzania i sprawdzania ograniczeń spodziewałem się timerange
zaoszczędzić 8 bajtów pamięci w porównaniu z tsrange
od time
zajmuje tylko 4 bajty. Ale okazuje się, że timerange
zajmuje 22 bajty na dysku (25 w pamięci RAM), podobnie jak tsrange
(lub tstzrange
). Więc możesz iść z tsrange
również. Zasada zapytania i ograniczenia wykluczenia jest taka sama.
Zapytanie
Opakowane w funkcję SQL dla wygodnej obsługi parametrów:
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;
Zadzwoń:
SELECT * FROM f_next_free('2014-10-28 12:00'::timestamp, '1.5 h'::interval);
Skrzypce SQL w Postgresie 9.3 teraz.
Wyjaśnij
-
Funkcja przyjmuje
_start
timestamp
jako minimalny czas rozpoczęcia i_duration interval
. Uważaj, aby rozpoczęcie wykluczyć tylko wcześniejszy czas dzień, a nie kolejne dni. Najprościej, wystarczy dodać dzień i godzinę:t + d > _start
.
Aby zarezerwować rezerwację rozpoczynającą się „teraz”, po prostu przekażnow()::timestamp
:SELECT * FROM f_next_free(`now()::timestamp`, '1.5 h'::interval);
-
Podzapytanie
d
generuje dni zaczynając od wartości wejściowej_day
. Wyłączone święta. - Dni są połączone z możliwymi zakresami czasu wygenerowanymi w podzapytaniu
t
. - To jest połączone krzyżowo ze wszystkimi dostępnymi procesami roboczymi
w
. - Na koniec wyeliminuj wszystkich kandydatów, którzy kolidują z istniejącymi rezerwacjami, używając
NOT EXISTS
anti-semi-join, a w szczególności operator nakładania się&&
.
Powiązane:
- Jak wykonujesz matematykę na randkach, która ignoruje rok? (dla przykładu matematyki z datami)
- Zapobieganie przyleganiu /nakładające się wpisy z EXCLUDE w PostgreSQL
- Oblicz pracę godzin między 2 datami w PostgreSQL