Rozvržení tabulky
Přepracujte tabulku tak, aby ukládala otevírací dobu (provozní dobu) jako sadu tsrange
(rozsah timestamp without time zone
) hodnoty. Vyžaduje Postgres 9.2 nebo novější .
Vyberte si náhodný týden, abyste si stanovili otevírací dobu. Líbí se mi týden:
1996-01-01 (pondělí) do 1996-01-07 (neděle)
Toto je poslední přestupný rok, kdy 1. led shodou okolností připadá na pondělí. Ale v tomto případě to může být jakýkoli náhodný týden. Buďte důslední.
Nainstalujte si přídavný modul btree_gist
první:
CREATE EXTENSION btree_gist;
Viz:
- Ekvivalentní omezení vyloučení složeného z celého čísla a rozsahu
Potom vytvořte tabulku takto:
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]')
);
Ten jeden sloupec hours
nahradí všechny vaše sloupce:
opens_on, closes_on, opens_at, closes_at
Například provozní doba od středy, 18:30 do čtvrtka, 05:00 UTC se zadávají jako:
'[1996-01-03 18:30, 1996-01-04 05:00]'
Omezení vyloučení hoo_no_overlap
zabraňuje překrývajícím se záznamům v obchodě. Je implementován pomocí GiST indexu , který také podporuje naše dotazy. Podívejte se na kapitolu "Index a výkon" níže o strategiích indexování.
Kontrolní omezení hoo_bounds_inclusive
vynucuje inkluzivní hranice pro vaše rozsahy se dvěma pozoruhodnými důsledky:
- Vždy je zahrnut časový bod spadající přesně na spodní nebo horní hranici.
- Sousední položky stejného obchodu jsou fakticky zakázány. S inkluzivními hranicemi by se tyto „překrývaly“ a vylučovací omezení by vyvolalo výjimku. Sousední položky musí být místo toho sloučeny do jednoho řádku. Kromě případů, kdy zabalí kolem nedělní půlnoci , v takovém případě musí být rozděleny do dvou řad. Funkce
f_hoo_hours()
níže se o to postará.
Kontrolní omezení hoo_standard_week
vynucuje vnější hranice přípravného týdne pomocí operátoru "rozsah obsahuje" <@
.
S včetně hranice, musíte dodržet rohový případ kde se čas otočí kolem nedělní půlnoci:
'1996-01-01 00:00+0' = '1996-01-08 00:00+0'
Mon 00:00 = Sun 24:00 (= next Mon 00:00)
Musíte hledat obě časová razítka najednou. Zde je související případ s exkluzivní horní mez, která by nevykazovala tento nedostatek:
- Prevence sousedících/překrývajících se položek pomocí EXCLUDE v PostgreSQL
Funkce f_hoo_time(timestamptz)
Chcete-li "normalizovat" jakékoli dané 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
pouze pro Postgres 9.6 nebo novější.
Funkce trvá timestamptz
a vrátí timestamp
. Přidá uplynulý interval příslušného týdne ($1 - date_trunc('week', $1)
v čase UTC do počátečního bodu našeho pracovního týdne. (date
+ interval
vytvoří timestamp
.)
Funkce f_hoo_hours(timestamptz, timestamptz)
Normalizovat rozsahy a rozdělit ty, které překračují Po 00:00. Tato funkce trvá libovolný interval (jako dva timestamp
) a vytvoří jeden nebo dva normalizované tsrange
hodnoty. Pokrývá jakékoli legální vstup a zbytek zakáže:
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$;
Chcete-li INSERT
single vstupní řádek:
INSERT INTO hoo(shop_id, hours)
SELECT 123, f_hoo_hours('2016-01-11 00:00+04', '2016-01-11 08:00+04');
Pro libovolné počet vstupních řádků:
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ždý může vložit dva řádky, pokud rozsah potřebuje rozdělení v Po 00:00 UTC.
Dotaz
S upraveným designem celý váš velký, složitý a drahý dotaz lze nahradit ... tímto:
SELECT *
FROM hoo
WHERE hours @> f_hoo_time(now());
Pro trochu napětí jsem přes roztok umístil spoiler. Přesuňte myši nad to.
Dotaz je podpořen uvedeným GiST indexem a je rychlý, dokonce i pro velké tabulky.
db<>zde hrajte (s dalšími příklady)
Starý sqlfiddle
Pokud si chcete spočítat celkovou otevírací dobu (na obchod), zde je recept:
- Vypočítejte pracovní dobu mezi 2 daty v PostgreSQL
Index a výkon
Operátor omezení pro typy rozsahů může být podporován pomocí GiST nebo SP-GiST index. Obojí lze použít k implementaci omezení vyloučení, ale pouze GiST podporuje vícesloupcové indexy:
V současné době podporují vícesloupcové indexy pouze indexy B-tree, GiST, GIN a BRIN.
A na pořadí sloupců indexu záleží:
Vícesloupcový index GiST lze použít s podmínkami dotazu, které zahrnují jakoukoli podmnožinu sloupců indexu. Podmínky v dalších sloupcích omezují položky vrácené indexem, ale podmínka v prvním sloupci je nejdůležitější pro určení, jak velkou část indexu je třeba prohledat. Index GiST bude relativně neúčinný, pokud jeho první sloupec obsahuje pouze několik odlišných hodnot, i když je v dalších sloupcích mnoho odlišných hodnot.
Máme tedy protichůdné zájmy tady. U velkých tabulek bude pro shop_id
mnohem více odlišných hodnot než za hours
.
- Index GiST s hlavním
shop_id
je rychlejší zapsat a vynutit omezení vyloučení. - Ale hledáme
hours
v našem dotazu. Mít tento sloupec jako první by bylo lepší. - Pokud potřebujeme vyhledat
shop_id
v jiných dotazech je prostý index btree mnohem rychlejší. - Aby toho nebylo málo, našel jsem SP-GiST index za pouhých
hours
být nejrychlejší pro dotaz.
Srovnávací
Nový test s Postgres 12 na starém notebooku. Můj skript pro generování fiktivních dat:
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;
Výsledkem je ~ 141 000 náhodně vygenerovaných řádků, ~ 30 000 různých shop_id
, ~ 12 tisíc různých hours
. Velikost tabulky 8 MB.
Zahodil jsem a znovu vytvořil omezení vyloučení:
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
první je ~ 4x rychlejší pro tuto distribuci.
Kromě toho jsem testoval další dva pro výkon čtení:
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;
, provedl jsem dva dotazy:
- Q1 :pozdě v noci, nalezení pouze 35 řádků
- 2. čtvrtletí :odpoledne, hledání 4547 řádků .
Výsledky
Mám kontrolu pouze indexu pro každý (samozřejmě kromě "bez indexu"):
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 a GiST jsou na stejné úrovni pro dotazy s malým počtem výsledků (GiST je ještě rychlejší pro velmi málo).
- SP-GiST se lépe škáluje s rostoucím počtem výsledků a je také menší.
Pokud čtete mnohem více, než píšete (typický případ použití), dodržujte omezení vyloučení, jak bylo navrženo na začátku, a vytvořte další index SP-GiST pro optimalizaci výkonu čtení.