Адаптирана схема
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 сега.
Обяснете
-
Функцията приема
_starttimestampкато минимално начално време и_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