Адаптирана схема
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
по-малък, индекси по-малки, заявки по-бързи, ... -
Актуализация: За по-евтино съхранение (8 байта вместо 22) и по-лесно боравене запазвам началото и края като
time
сега и конструирайте диапазон в движение за ограничението за изключване:EXCLUDE USING gist (worker_id WITH =, day WITH = , timerange(work_from, work_to) WITH &&)
-
Тъй като вашите диапазони никога не могат да преминат границата на датата по дефиниция би било по-ефективно да има отделна
date
колона (day
в моето изпълнение) и времеви диапазон . Типътtimerange
не се доставя в инсталациите по подразбиране, но се създава лесно. По този начин можете значително да опростите вашите ограничения за проверка. -
Използвайте
EXTRACT('isodow', ...)
за опростяване, изключвайки неделя -
Предполагам, че искате да разрешите горната граница на „21:00“.
-
Предполага се, че границите са включващи за долната и изключващи за горната граница.
-
Проверката дали нови/актуализирани дни са в рамките на един месец от „сега“ не е
IMMUTABLE
. Преместихте го отCHECK
ограничение за тригера - в противен случай може да се натъкнете на проблеми с изхвърляне / възстановяване! Подробности:
Настрани
Освен опростяване на ограниченията за въвеждане и проверка очаквах timerange
за да спестите 8 байта място за съхранение в сравнение с tsrange
от time
заема само 4 байта. Но се оказва timerange
заема 22 байта на диска (25 в RAM), точно като tsrange
(или 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 Fiddle на Postgres 9.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
анти-полусъединяване, и по-специално операторът за припокриване&&
.
Свързани:
- Как се прави математика за дата, която игнорира годината? (за математически пример за дата)
- Предотвратяване на съседни /припокриващи се записи с EXCLUDE в PostgreSQL
- Изчислете работните часа между 2 дати в PostgreSQL