Układ tabeli
Przeprojektuj stół, aby przechowywać godziny otwarcia (godziny pracy) jako zestaw tsrange
(zakres timestamp without time zone
) wartości. Wymaga Postgresa 9.2 lub nowszego .
Wybierz losowy tydzień, aby ustawić godziny otwarcia. Lubię tydzień:
1996-01-01 (poniedziałek) do 07.01.1996 (niedziela)
To ostatni rok przestępny, w którym 1 stycznia wypada w poniedziałek. Ale w tym przypadku może to być dowolny losowy tydzień. Po prostu bądź konsekwentny.
Zainstaluj dodatkowy moduł btree_gist
po pierwsze:
CREATE EXTENSION btree_gist;
Zobacz:
- Równoważne z ograniczeniem wykluczenia złożonym z liczby całkowitej i zakresu
Następnie utwórz tabelę w ten sposób:
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]')
);
jeden kolumna hours
zastępuje wszystkie Twoje kolumny:
opens_on, closes_on, opens_at, closes_at
Na przykład godziny pracy od środa, 18:30 do czwartku, 05:00 UTC są wprowadzane jako:
'[1996-01-03 18:30, 1996-01-04 05:00]'
Ograniczenie wykluczenia hoo_no_overlap
zapobiega nakładaniu się wpisów na sklep. Jest zaimplementowany z indeksem GiST , które również wspierają nasze zapytania. Rozważ rozdział „Indeks i wydajność” poniżej omawiając strategie indeksowania.
Ograniczenie sprawdzające hoo_bounds_inclusive
wymusza inkluzywne granice dla twoich zakresów, z dwiema godnymi uwagi konsekwencjami:
- Zawsze uwzględniany jest punkt w czasie przypadający dokładnie na dolną lub górną granicę.
- Sąsiadujące wpisy dla tego samego sklepu są skutecznie zabronione. W przypadku granic inkluzywnych te „nachodziłyby na siebie”, a ograniczenie wykluczenia powodowałoby wyjątek. Sąsiednie wpisy należy zamiast tego scalić w jeden wiersz. Chyba że zawijają się w niedzielę o północy , w takim przypadku muszą być podzielone na dwa rzędy. Funkcja
f_hoo_hours()
poniżej zajmuje się tym.
Ograniczenie sprawdzania hoo_standard_week
wymusza zewnętrzne granice tygodnia pomostowego przy użyciu operatora „zakres zawiera się w” <@
.
Z włącznie granice, musisz obserwować przypadek narożny gdzie czas się kończy o północy w niedzielę:
'1996-01-01 00:00+0' = '1996-01-08 00:00+0'
Mon 00:00 = Sun 24:00 (= next Mon 00:00)
Musisz wyszukać oba znaczniki czasu jednocześnie. Oto powiązana sprawa z ekskluzywnym górna granica, która nie wykazałaby tej wady:
- Zapobieganie sąsiednim/nakładającym się wpisom za pomocą EXCLUDE w PostgreSQL
Funkcja f_hoo_time(timestamptz)
Aby "znormalizować" dowolny podany 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
tylko dla Postgresa 9.6 lub nowszego.
Funkcja przyjmuje timestamptz
i zwraca timestamp
. Dodaje interwał, który upłynął w danym tygodniu ($1 - date_trunc('week', $1)
w czasie UTC do punktu rozpoczęcia naszego tygodnia inscenizacji. (date
+ interval
generuje timestamp
.)
Funkcja f_hoo_hours(timestamptz, timestamptz)
Aby znormalizować zakresy i podzielić te, które przecinają Mon 00:00. Ta funkcja zajmuje dowolny interwał (jako dwa timestamptz
) i tworzy jeden lub dwa znormalizowane tsrange
wartości. Obejmuje dowolne legalny wkład i nie zezwala na resztę:
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$;
Aby INSERT
singiel wiersz wejściowy:
INSERT INTO hoo(shop_id, hours)
SELECT 123, f_hoo_hours('2016-01-11 00:00+04', '2016-01-11 08:00+04');
Dla dowolnych liczba wierszy wejściowych:
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);
Każdy może wstawić dwa wiersze, jeśli zakres wymaga podziału o godz. 00:00 UTC.
Zapytanie
Dzięki dostosowanemu projektowi całe duże, złożone i kosztowne zapytanie można zastąpić ... to:
SELECT *
FROM hoo
WHERE hours @> f_hoo_time(now());
Dla trochę suspensu nałożyłem na roztwór płytę spoilera. Przesuń mysz nad to.
Zapytanie jest obsługiwane przez wspomniany indeks GiST i jest szybkie, nawet w przypadku dużych tabel.
db<>graj tutaj (więcej przykładów)
Stary sqlfiddle
Jeśli chcesz obliczyć łączne godziny otwarcia (na sklep), oto przepis:
- Oblicz godziny pracy między 2 datami w PostgreSQL
Indeks i wydajność
Operator zawierania dla typów zasięgu może być obsługiwany przez GiST lub SP-GiST indeks. Każdego można użyć do zaimplementowania ograniczenia wykluczenia, ale tylko GiST obsługuje indeksy wielokolumnowe:
Obecnie tylko typy indeksów B-drzewo, GiST, GIN i BRIN obsługują indeksy wielokolumnowe.
A kolejność kolumn indeksu ma znaczenie:
Wielokolumnowy indeks GiST może być używany z warunkami zapytania, które obejmują dowolny podzbiór kolumn indeksu. Warunki w dodatkowych kolumnach ograniczają wpisy zwracane przez indeks, ale warunek w pierwszej kolumnie jest najważniejszy dla określenia, jaka część indeksu musi zostać przeskanowana. Indeks GiST będzie stosunkowo nieefektywny, jeśli jego pierwsza kolumna ma tylko kilka odrębnych wartości, nawet jeśli w dodatkowych kolumnach jest wiele odrębnych wartości.
Mamy więc sprzeczne interesy tutaj. W przypadku dużych tabel będzie dużo więcej odrębnych wartości dla shop_id
niż przez hours
.
- Indeks GiST z wiodącym
shop_id
jest szybszy w pisaniu i egzekwowaniu ograniczenia wykluczenia. - Ale szukamy
hours
w naszym zapytaniu. Lepiej byłoby mieć tę kolumnę jako pierwszą. - Jeśli musimy wyszukać
shop_id
w innych zapytaniach zwykły indeks btree jest do tego znacznie szybszy. - Na domiar złego znalazłem SP-GiST indeksuj tylko
hours
być najszybszym dla zapytania.
Wzorzec
Nowy test z Postgresem 12 na starym laptopie.Mój skrypt do generowania fikcyjnych danych:
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;
Wyniki w ~141k losowo wygenerowanych wierszy, ~30k odrębnych shop_id
, ~ 12 tys. odrębnych hours
. Rozmiar stołu 8 MB.
Usunąłem i odtworzyłem ograniczenie wykluczenia:
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
pierwszy jest ~ 4x szybszy dla tej dystrybucji.
Ponadto przetestowałem jeszcze dwa pod kątem wydajności odczytu:
CREATE INDEX hoo_hours_gist_idx on hoo USING gist (hours);
CREATE INDEX hoo_hours_spgist_idx on hoo USING spgist (hours); -- !!
Po VACUUM FULL ANALYZE hoo;
, uruchomiłem dwa zapytania:
- Pierwszy kwartał :późna noc, znajdowanie tylko 35 wierszy
- II kwartał :po południu, znajdowanie 4547 wierszy .
Wyniki
Otrzymałem skanowanie tylko z indeksem dla każdego (oczywiście z wyjątkiem "bez indeksu"):
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 i GiST są takie same w przypadku zapytań, które znajdują niewiele wyników (GiST jest jeszcze szybszy w przypadku bardzo kilka).
- SP-GiST skaluje się lepiej wraz ze wzrostem liczby wyników, a także jest mniejszy.
Jeśli czytasz dużo więcej niż piszesz (typowy przypadek użycia), zachowaj ograniczenie wykluczenia zgodnie z sugestią na początku i utwórz dodatkowy indeks SP-GiST, aby zoptymalizować wydajność odczytu.