Lược đồ đã điều chỉnh
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();
Những điểm chính
-
Không sử dụng
char(n)
varchar(n)
hoặc tốt hơn,varchar
hoặc chỉtext
. -
Không sử dụng tên của nhân viên làm khóa chính. Nó không nhất thiết phải là duy nhất và có thể thay đổi. Thay vào đó, hãy sử dụng khóa chính thay thế, tốt nhất là
serial
. Đồng thời tạo các mục nhập trongreservat
nhỏ hơn, lập chỉ mục nhỏ hơn, truy vấn nhanh hơn, ... -
Cập nhật: Để có bộ nhớ rẻ hơn (8 byte thay vì 22) và xử lý đơn giản hơn, tôi lưu phần bắt đầu và kết thúc dưới dạng
time
ngay bây giờ và xây dựng một phạm vi nhanh chóng cho ràng buộc loại trừ:EXCLUDE USING gist (worker_id WITH =, day WITH = , timerange(work_from, work_to) WITH &&)
-
Vì phạm vi của bạn có thể không bao giờ vượt qua biên giới ngày theo định nghĩa, sẽ hiệu quả hơn nếu có một
date
riêng biệt cột (day
trong quá trình triển khai của tôi) và phạm vi thời gian . Loạitimerange
không được vận chuyển trong cài đặt mặc định, nhưng được tạo dễ dàng. Bằng cách này, bạn có thể đơn giản hóa phần lớn các ràng buộc kiểm tra của mình. -
Sử dụng
EXTRACT('isodow', ...)
để đơn giản hóa việc loại trừ các ngày chủ nhật -
Tôi giả sử bạn muốn cho phép đường viền trên của '21:00 '.
-
Các đường viền được giả định là bao gồm cho giới hạn dưới và loại trừ cho giới hạn trên.
-
Việc kiểm tra xem các ngày mới / cập nhật có nằm trong vòng một tháng kể từ "bây giờ" không
IMMUTABLE
. Đã chuyển nó từCHECK
ràng buộc với trình kích hoạt - nếu không, bạn có thể gặp sự cố với kết xuất / khôi phục! Chi tiết:
Bên cạnh
Ngoài việc đơn giản hóa đầu vào và kiểm tra các ràng buộc mà tôi mong đợi timerange
để tiết kiệm 8 byte dung lượng so với tsrange
kể từ time
chỉ chiếm 4 byte. Nhưng hóa ra timerange
chiếm 22 byte trên đĩa (25 trong RAM), giống như tsrange
(hoặc tstzrange
). Vì vậy, bạn có thể sử dụng tsrange
cũng. Nguyên tắc của truy vấn và ràng buộc loại trừ giống nhau.
Truy vấn
Được bao bọc trong một hàm SQL để xử lý tham số thuận tiện:
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;
Gọi:
SELECT * FROM f_next_free('2014-10-28 12:00'::timestamp, '1.5 h'::interval);
SQL Fiddle trên Postgres 9.3 ngay bây giờ.
Giải thích
-
Hàm nhận
_start
timestamp
là thời gian bắt đầu tối thiểu và_duration interval
. Hãy cẩn thận chỉ loại trừ thời gian sớm hơn vào bắt đầu ngày, không phải những ngày sau. Đơn giản nhất chỉ bằng cách thêm ngày và giờ:t + d > _start
.
Để đặt chỗ bắt đầu từ "bây giờ", chỉ cần chuyểnnow()::timestamp
:SELECT * FROM f_next_free(`now()::timestamp`, '1.5 h'::interval);
-
Truy vấn con
d
tạo ngày bắt đầu từ giá trị đầu vào_day
. Không bao gồm các ngày lễ. - Các ngày được kết hợp chéo với các phạm vi thời gian có thể được tạo trong truy vấn con
t
. - Điều đó được kết hợp chéo với tất cả các nhân viên hiện có
w
. - Cuối cùng, loại bỏ tất cả các ứng cử viên xung đột với các đặt chỗ hiện có bằng cách sử dụng
NOT EXISTS
chống bán nối và đặc biệt là toán tử chồng chéo&&
.
Có liên quan:
- Làm thế nào để bạn thực hiện phép toán ngày tháng bỏ qua năm? (ví dụ về toán học ngày tháng)
- Ngăn liền kề / các mục nhập chồng chéo với EXCLUDE trong PostgreSQL
- Tính toán hoạt động giờ giữa 2 ngày trong PostgreSQL