sql >> Databáze >  >> RDS >> PostgreSQL

Proveďte tento dotaz na hodiny provozu v PostgreSQL

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í.




  1. Funkce TAN() v Oracle

  2. Vypočítat průběžný součet v SQL Server

  3. SQL dotaz „LIKE“ s použitím „%“, kde vyhledávací kritéria obsahují „%“

  4. MayBeSQL přichází do Microsoft Access!