Tabellayout
Re-design tabellen til at gemme åbningstider (åbningstider) som et sæt tsrange
(interval af timestamp without time zone
) værdier. Kræver Postgres 9.2 eller nyere .
Vælg en tilfældig uge til at planlægge dine åbningstider. Jeg kan lide ugen:
1996-01-01 (mandag) til 1996-01-07 (søndag)
Det er det seneste skudår, hvor 1. januar passende er en mandag. Men det kan være en hvilken som helst tilfældig uge for denne sag. Bare vær konsekvent.
Installer det ekstra modul btree_gist
først:
CREATE EXTENSION btree_gist;
Se:
- Svarer til ekskluderingsbegrænsning sammensat af heltal og interval
Opret derefter tabellen sådan her:
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]')
);
Den en kolonne hours
erstatter alle dine kolonner:
opens_on, closes_on, opens_at, closes_at
For eksempel åbningstider fra onsdag kl. 18.30 til torsdag kl. 05:00 UTC indtastes som:
'[1996-01-03 18:30, 1996-01-04 05:00]'
Ekskluderingsbegrænsningen hoo_no_overlap
forhindrer overlappende poster pr. butik. Det er implementeret med et GiST-indeks , hvilket også tilfældigvis understøtter vores forespørgsler. Overvej kapitlet "Indeks og ydeevne" nedenfor diskuterer indekseringsstrategier.
Kontrolbegrænsningen hoo_bounds_inclusive
håndhæver inkluderende grænser for dine områder med to bemærkelsesværdige konsekvenser:
- Et tidspunkt, der falder nøjagtigt på den nedre eller øvre grænse, er altid inkluderet.
- Tilstødende tilmeldinger til den samme butik er i praksis forbudt. Med inklusiv grænser ville disse "overlappe", og udelukkelsesbegrænsningen ville give anledning til en undtagelse. Tilstødende poster skal i stedet flettes til en enkelt række. Undtagen når de ombrydes omkring midnat søndag , i så fald skal de opdeles i to rækker. Funktionen
f_hoo_hours()
nedenfor tager sig af dette.
Kontrolbegrænsningen hoo_standard_week
håndhæver de ydre grænser for iscenesættelsesugen ved hjælp af "rækkevidde er indeholdt af" operatoren <@
.
Med inklusive grænser, skal du observere en hjørnekasse hvor tiden går rundt ved midnat søndag:
'1996-01-01 00:00+0' = '1996-01-08 00:00+0'
Mon 00:00 = Sun 24:00 (= next Mon 00:00)
Du skal søge efter begge tidsstempler på én gang. Her er en relateret sag med eksklusiv øvre grænse, der ikke ville udvise denne mangel:
- Forebyggelse af tilstødende/overlappende poster med EXCLUDE i PostgreSQL
Funktion f_hoo_time(timestamptz)
At "normalisere" et givet 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
kun til Postgres 9.6 eller nyere.
Funktionen tager timestamptz
og returnerer timestamp
. Den tilføjer det forløbne interval for den respektive uge ($1 - date_trunc('week', $1)
i UTC-tid til startpunktet for vores iscenesættelsesuge. (date
+ interval
producerer timestamp
.)
Funktion f_hoo_hours(timestamptz, timestamptz)
For at normalisere intervaller og opdele dem, der krydser man kl. 00:00. Denne funktion tager ethvert interval (som to timestamptz
) og producerer en eller to normaliserede tsrange
værdier. Det dækker enhver juridisk input og forbyder resten:
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$;
For at INSERT
en enkelt input række:
INSERT INTO hoo(shop_id, hours)
SELECT 123, f_hoo_hours('2016-01-11 00:00+04', '2016-01-11 08:00+04');
For enhver antal inputrækker:
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);
Hver kan indsætte to rækker, hvis et område skal opdeles ved man kl. 00:00 UTC.
Forespørgsel
Med det tilpassede design, din hele store, komplekse, dyre forespørgsel kan erstattes med ... denne:
SELECT *
FROM hoo
WHERE hours @> f_hoo_time(now());
For lidt spænding satte jeg en spoilerplade over opløsningen. Flyt musen over det.
Forespørgslen er understøttet af nævnte GiST-indeks og hurtig, selv for store borde.
db<>spil her (med flere eksempler)
Gamle sqlfiddle
Ønsker du at beregne samlede åbningstider (pr. butik), er her en opskrift:
- Beregn arbejdstimer mellem 2 datoer i PostgreSQL
Indeks og ydeevne
Indeslutningsoperatøren for områdetyper kan understøttes med en GiST eller SP-GiST indeks. Begge kan bruges til at implementere en ekskluderingsbegrænsning, men kun GiST understøtter indekser med flere kolonner:
I øjeblikket er det kun B-tree-, GiST-, GIN- og BRIN-indekstyperne, der understøtter indekser med flere kolonner.
Og rækkefølgen af indekskolonner har betydning:
Et GiST-indeks med flere kolonner kan bruges med forespørgselsbetingelser, der involverer en hvilken som helst delmængde af indeksets kolonner. Betingelser for yderligere kolonner begrænser de poster, der returneres af indekset, men betingelsen i den første kolonne er den vigtigste for at bestemme, hvor meget af indekset, der skal scannes. Et GiST-indeks vil være relativt ineffektivt, hvis dets første kolonne kun har nogle få distinkte værdier, selvom der er mange distinkte værdier i yderligere kolonner.
Så vi har modstridende interesser her. For store borde vil der være mange flere forskellige værdier for shop_id
end i hours
.
- Et GiST-indeks med førende
shop_id
er hurtigere at skrive og håndhæve udelukkelsesbegrænsningen. - Men vi søger efter
hours
i vores forespørgsel. Det ville være bedre at have den kolonne først. - Hvis vi skal slå
shop_id
op i andre forespørgsler er et almindeligt btree-indeks meget hurtigere til det. - Over det hele fandt jeg en SP-GiST indeks på kun
hours
at være hurtigst for forespørgslen.
Benchmark
Ny test med Postgres 12 på en gammel bærbar computer. Mit script til at generere dummy-data:
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;
Resulterer i ~ 141.000 tilfældigt genererede rækker, ~ 30.000 distinkt shop_id
, ~ 12.000 forskellige hours
. Bordstørrelse 8 MB.
Jeg droppede og genskabte ekskluderingsbegrænsningen:
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
første er ~ 4x hurtigere for denne distribution.
Derudover testede jeg to mere til læseydelse:
CREATE INDEX hoo_hours_gist_idx on hoo USING gist (hours);
CREATE INDEX hoo_hours_spgist_idx on hoo USING spgist (hours); -- !!
Efter VACUUM FULL ANALYZE hoo;
, jeg kørte to forespørgsler:
- 1. kvartal :sen nat, finder kun 35 rækker
- Q2 :om eftermiddagen, finde 4547 rækker .
Resultater
Fik en kun-indeksscanning for hver (undtagen "intet indeks", selvfølgelig):
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 og GiST er på niveau med forespørgsler med få resultater (GiST er endnu hurtigere for meget få).
- SP-GiST skalerer bedre med et stigende antal resultater og er også mindre.
Hvis du læser meget mere, end du skriver (typisk brug), skal du beholde udelukkelsesbegrænsningen som foreslået i starten og oprette et ekstra SP-GiST-indeks for at optimere læseydelsen.