Оформление на таблицата
Препроектирайте таблицата за съхраняване на работното време (часове на работа) като набор от tsrange
(диапазон от timestamp without time zone
) стойности. Изисква Postgres 9.2 или по-нова версия .
Изберете произволна седмица, за да поставите работното си време. Харесвам седмицата:
1996-01-01 (понеделник) до 1996-01-07 (неделя)
Това е най-новата високосна година, в която 1 януари удобно е понеделник. Но това може да бъде произволна седмица за този случай. Просто бъдете последователни.
Инсталирайте допълнителния модул btree_gist
първо:
CREATE EXTENSION btree_gist;
Вижте:
- Еквивалентно на ограничение за изключване, съставено от цяло число и диапазон
След това създайте таблицата по следния начин:
CREATE TABLE hoo (
hoo_id serial PRIMARY KEY
, shop_id int NOT NULL -- REFERENCES shop(shop_id) -- reference to shop
, hours tsrange NOT NULL
, CONSTRAINT hoo_no_overlap EXCLUDE USING gist (shop_id with =, hours WITH &&)
, CONSTRAINT hoo_bounds_inclusive CHECK (lower_inc(hours) AND upper_inc(hours))
, CONSTRAINT hoo_standard_week CHECK (hours <@ tsrange '[1996-01-01 0:0, 1996-01-08 0:0]')
);
единият колона hours
замества всичките ви колони:
opens_on, closes_on, opens_at, closes_at
Например работно време от сряда, 18:30 до четвъртък, 05:00 ч. UTC се въвеждат като:
'[1996-01-03 18:30, 1996-01-04 05:00]'
Ограничението за изключване hoo_no_overlap
предотвратява припокриването на вписванията на магазин. Реализира се с индекс GiST , което също подкрепя нашите запитвания. Помислете за главата „Индекс и производителност“ по-долу се обсъждат стратегиите за индексиране.
Ограничението за проверка hoo_bounds_inclusive
налага приобщаващи граници за вашите диапазони с две забележителни последици:
- Винаги се включва момент от време, попадащ точно върху долната или горната граница.
- Съседните записи за един и същи магазин на практика са забранени. С включващи граници, те биха се „припокривали“ и ограничението за изключване би предизвикало изключение. Вместо това съседните записи трябва да бъдат обединени в един ред. Освен когато те увиват около полунощ в неделя , като в този случай те трябва да бъдат разделени на два реда. Функцията
f_hoo_hours()
по-долу се грижи за това.
Ограничението за проверка hoo_standard_week
налага външните граници на етапната седмица, като използва оператора <@
"диапазонът се съдържа от" .
Свключително граници, трябва да спазвате ъглов случай където времето приключва в неделя полунощ:
'1996-01-01 00:00+0' = '1996-01-08 00:00+0'
Mon 00:00 = Sun 24:00 (= next Mon 00:00)
Трябва да потърсите и двете времеви марки наведнъж. Ето един свързан случай сизключително горна граница, която не показва този недостатък:
- Предотвратяване на съседни/припокриващи се записи с EXCLUDE в PostgreSQL
Функция f_hoo_time(timestamptz)
За да "нормализирате" произволен timestamp with time zone
:
CREATE OR REPLACE FUNCTION f_hoo_time(timestamptz)
RETURNS timestamp
LANGUAGE sql IMMUTABLE PARALLEL SAFE AS
$func$
SELECT timestamp '1996-01-01' + ($1 AT TIME ZONE 'UTC' - date_trunc('week', $1 AT TIME ZONE 'UTC'))
$func$;
PARALLEL SAFE
само за Postgres 9.6 или по-нова версия.
Функцията приема timestamptz
и връща timestamp
. Той добавя изтеклия интервал от съответната седмица ($1 - date_trunc('week', $1)
по UTC до началната точка на нашата постановителна седмица. (date
+ interval
произвежда timestamp
.)
Функция f_hoo_hours(timestamptz, timestamptz)
За нормализиране на диапазони и разделяне на тези, които пресичат поне 00:00. Тази функция приема произволен интервал (като два timestamptz
) и произвежда един или два нормализирани tsrange
стойности. Покривавсякото законно въвеждане и забранява останалите:
CREATE OR REPLACE FUNCTION f_hoo_hours(_from timestamptz, _to timestamptz)
RETURNS TABLE (hoo_hours tsrange)
LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE COST 500 ROWS 1 AS
$func$
DECLARE
ts_from timestamp := f_hoo_time(_from);
ts_to timestamp := f_hoo_time(_to);
BEGIN
-- sanity checks (optional)
IF _to <= _from THEN
RAISE EXCEPTION '%', '_to must be later than _from!';
ELSIF _to > _from + interval '1 week' THEN
RAISE EXCEPTION '%', 'Interval cannot span more than a week!';
END IF;
IF ts_from > ts_to THEN -- split range at Mon 00:00
RETURN QUERY
VALUES (tsrange('1996-01-01', ts_to , '[]'))
, (tsrange(ts_from, '1996-01-08', '[]'));
ELSE -- simple case: range in standard week
hoo_hours := tsrange(ts_from, ts_to, '[]');
RETURN NEXT;
END IF;
RETURN;
END
$func$;
За INSERT
единична ред за въвеждане:
INSERT INTO hoo(shop_id, hours)
SELECT 123, f_hoo_hours('2016-01-11 00:00+04', '2016-01-11 08:00+04');
За всякакви брой входни редове:
INSERT INTO hoo(shop_id, hours)
SELECT id, f_hoo_hours(f, t)
FROM (
VALUES (7, timestamptz '2016-01-11 00:00+0', timestamptz '2016-01-11 08:00+0')
, (8, '2016-01-11 00:00+1', '2016-01-11 08:00+1')
) t(id, f, t);
Всеки може да вмъкне два реда, ако диапазонът се нуждае от разделяне в понеделник 00:00 UTC.
Запитване
С коригирания дизайн, цялата ви голяма, сложна, скъпа заявка може да бъде заменен с ... това:
SELECT *
FROM hoo
WHERE hours @> f_hoo_time(now());
За малко напрежение поставих спойлерна плоча върху разтвора. Преместете мишката над то.
Заявката е подкрепена от посочения GiST индекс и е бърза, дори за големи таблици.
db<>цигулка тук (с още примери)
Стар sqlfiddle
Ако искате да изчислите общо работно време (за магазин), ето една рецепта:
- Изчислете работното време между 2 дати в PostgreSQL
Индекс и производителност
Операторът за ограничаване за типове диапазони може да се поддържа с GiST или SP-GiST индекс. И двете могат да се използват за прилагане на ограничение за изключване, но само GiST поддържа многоколонни индекси:
Понастоящем само индексите B-tree, GiST, GIN и BRIN поддържат индекси с няколко колони.
И редът на индексните колони има значение:
GiST индекс с много колони може да се използва с условия на заявка, които включват всяко подмножество от колоните на индекса. Условията в допълнителните колони ограничават записите, върнати от индекса, но условието на първата колона е най-важното за определяне каква част от индекса трябва да бъде сканирана. GiST индексът ще бъде относително неефективен, ако първата му колона има само няколко различни стойности, дори ако има много различни стойности в допълнителни колони.
Така че имаме конфликтни интереси тук. За големи таблици ще има много повече различни стойности за shop_id
отколкото за hours
.
- Индекс GiST с водещ
shop_id
е по-бърз за писане и за налагане на ограничението за изключване. - Но ние търсим
hours
в нашето запитване. Ще бъде по-добре първо да имате тази колона. - Ако трябва да потърсим
shop_id
в други заявки обикновен индекс на btree е много по-бърз за това. - Като капак намерих SP-GiST индекс само за
hours
да бъденай-бързи за заявката.
Сравнение
Нов тест с Postgres 12 на стар лаптоп. Моят скрипт за генериране на фиктивни данни:
INSERT INTO hoo(shop_id, hours)
SELECT id
, f_hoo_hours(((date '1996-01-01' + d) + interval '4h' + interval '15 min' * trunc(32 * random())) AT TIME ZONE 'UTC'
, ((date '1996-01-01' + d) + interval '12h' + interval '15 min' * trunc(64 * random() * random())) AT TIME ZONE 'UTC')
FROM generate_series(1, 30000) id
JOIN generate_series(0, 6) d ON random() > .33;
Резултати до ~ 141 000 произволно генерирани реда, ~ 30 000 различни shop_id
, ~ 12k различни hours
. Размер на таблицата 8 MB.
Изхвърлих и пресъздадох ограничението за изключване:
ALTER TABLE hoo
DROP CONSTRAINT hoo_no_overlap
, ADD CONSTRAINT hoo_no_overlap EXCLUDE USING gist (shop_id WITH =, hours WITH &&); -- 3.5 sec; index 8 MB
ALTER TABLE hoo
DROP CONSTRAINT hoo_no_overlap
, ADD CONSTRAINT hoo_no_overlap EXCLUDE USING gist (hours WITH &&, shop_id WITH =); -- 13.6 sec; index 12 MB
shop_id
първото е ~ 4 пъти по-бързо за тази дистрибуция.
Освен това тествах още две за производителност на четене:
CREATE INDEX hoo_hours_gist_idx on hoo USING gist (hours);
CREATE INDEX hoo_hours_spgist_idx on hoo USING spgist (hours); -- !!
След VACUUM FULL ANALYZE hoo;
, изпълних две заявки:
- Q1 :късно през нощта, намирам само 35 реда
- Q2 :следобед, намиране на 4547 реда .
Резултати
Получих сканиране само за индекс за всеки (с изключение на "без индекс", разбира се):
index idx size Q1 Q2
------------------------------------------------
no index 38.5 ms 38.5 ms
gist (shop_id, hours) 8MB 17.5 ms 18.4 ms
gist (hours, shop_id) 12MB 0.6 ms 3.4 ms
gist (hours) 11MB 0.3 ms 3.1 ms
spgist (hours) 9MB 0.7 ms 1.8 ms -- !
- SP-GiST и GiST са на ниво за заявки, намиращи малко резултати (GiST е дори по-бърз за много малко).
- SP-GiST се мащабира по-добре с нарастващ брой резултати и също е по-малък.
Ако четете много повече, отколкото пишете (типичен случай на употреба), запазете ограничението за изключване, както беше предложено в началото, и създайте допълнителен SP-GiST индекс, за да оптимизирате производителността на четене.