PostgreSQL
 sql >> Database >  >> RDS >> PostgreSQL

Esegui questa query sulle ore di funzionamento in PostgreSQL

Disposizione della tabella

Ridisegna la tabella per memorizzare gli orari di apertura (ore di apertura) come un insieme di tsrange (intervallo di timestamp without time zone ) valori. Richiede Postgres 9.2 o versioni successive .

Scegli una settimana a caso per mettere in scena i tuoi orari di apertura. Mi piace la settimana:
1996-01-01 (lunedì) al 07-01-1996 (domenica)
Questo è l'anno bisestile più recente in cui il 1° gennaio sembra convenientemente essere un lunedì. Ma può essere una qualsiasi settimana casuale per questo caso. Sii coerente.

Installa il modulo aggiuntivo btree_gist primo:

CREATE EXTENSION btree_gist;

Vedi:

  • Equivalente al vincolo di esclusione composto da numero intero e intervallo

Quindi crea la tabella in questo modo:

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]')
);

Quello uno colonna hours sostituisce tutte le tue colonne:

opens_on, closes_on, opens_at, closes_at

Ad esempio, gli orari di apertura da mercoledì, 18:30 a giovedì alle 05:00 UTC sono inseriti come:

'[1996-01-03 18:30, 1996-01-04 05:00]'

Il vincolo di esclusione hoo_no_overlap previene la sovrapposizione di voci per negozio. È implementato con un indice GiST , che supporta anche le nostre domande. Considera il capitolo "Indice e rendimento" di seguito discutendo le strategie di indicizzazione.

Il vincolo di controllo hoo_bounds_inclusive impone limiti inclusivi per i tuoi intervalli, con due conseguenze degne di nota:

  • Un momento che cade esattamente sul limite inferiore o superiore è sempre incluso.
  • Le voci adiacenti per lo stesso negozio sono effettivamente non consentite. Con limiti inclusivi, quelli "si sovrapporrebbero" e il vincolo di esclusione solleverebbe un'eccezione. Le voci adiacenti devono invece essere unite in un'unica riga. Tranne quando si avvolgono intorno alla mezzanotte di domenica , nel qual caso devono essere divisi in due righe. La funzione f_hoo_hours() di seguito si occupa di questo.

Il vincolo di controllo hoo_standard_week applica i limiti esterni della settimana di staging utilizzando l'operatore "l'intervallo è contenuto da" <@ .

Con inclusivo limiti, devi osservare un caso d'angolo dove il tempo finisce a mezzanotte di domenica:

'1996-01-01 00:00+0' = '1996-01-08 00:00+0'
 Mon 00:00 = Sun 24:00 (= next Mon 00:00)

Devi cercare entrambi i timestamp contemporaneamente. Ecco un caso correlato con esclusiva limite superiore che non presenterebbe questa mancanza:

  • Prevenire voci adiacenti/sovrapposte con EXCLUDE in PostgreSQL

Funzione f_hoo_time(timestamptz)

Per "normalizzare" un dato 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 solo per Postgres 9.6 o successivo.

La funzione accetta timestamptz e restituisce timestamp . Aggiunge l'intervallo trascorso della rispettiva settimana ($1 - date_trunc('week', $1) in ora UTC al punto di inizio della nostra settimana di sosta. (date + interval produce timestamp .)

Funzione f_hoo_hours(timestamptz, timestamptz)

Per normalizzare gli intervalli e dividere quelli che attraversano lun 00:00. Questa funzione prende qualsiasi intervallo (come due timestamptz ) e produce uno o due tsrange normalizzati valori. Copre qualsiasi input legale e non consente il resto:

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$;

Per INSERT un single riga di immissione:

INSERT INTO hoo(shop_id, hours)
SELECT 123, f_hoo_hours('2016-01-11 00:00+04', '2016-01-11 08:00+04');

Per qualsiasi numero di righe di input:

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);

Ciascuno può inserire due righe se un intervallo deve essere suddiviso alle 00:00 UTC di lunedì.

Interrogazione

Con il design modificato, tutta la tua query grande, complessa e costosa può essere sostituito con ... questo:

SELECT *
FROM hoo
WHERE hours @> f_hoo_time(now());

Per un po' di suspense ho messo una piastra spoiler sopra la soluzione. Sposta il mouse sopra esso.

La query è supportata da detto indice GiST e veloce, anche per tabelle di grandi dimensioni.

db<>gioca qui (con altri esempi)
Sqlfiddle vecchio

Se vuoi calcolare gli orari di apertura totali (per negozio), ecco una ricetta:

  • Calcola le ore di lavoro tra 2 date in PostgreSQL

Indice e performance

L'operatore di contenimento per i tipi di portata può essere supportato con un GiST o SP-GiST indice. Entrambi possono essere utilizzati per implementare un vincolo di esclusione, ma solo GiST supporta indici multicolonna:

Attualmente, solo i tipi di indice B-tree, GiST, GIN e BRIN supportano gli indici a più colonne.

E l'ordine delle colonne dell'indice è importante:

Un indice GiST multicolonna può essere utilizzato con condizioni di query che coinvolgono qualsiasi sottoinsieme delle colonne dell'indice. Le condizioni sulle colonne aggiuntive limitano le voci restituite dall'indice, ma la condizione sulla prima colonna è la più importante per determinare quanto dell'indice deve essere scansionato. Un indice GiST sarà relativamente inefficace se la sua prima colonna ha solo pochi valori distinti, anche se ci sono molti valori distinti in colonne aggiuntive.

Quindi abbiamo interessi contrastanti qui. Per le tabelle grandi, ci saranno molti più valori distinti per shop_id che per hours .

  • Un indice GiST con shop_id all'inizio è più veloce da scrivere e applicare il vincolo di esclusione.
  • Ma stiamo cercando hours nella nostra domanda. Avere prima quella colonna sarebbe meglio.
  • Se dobbiamo cercare shop_id in altre query, un semplice indice btree è molto più veloce per questo.
  • Per finire, ho trovato un SP-GiST indice a sole hours essere il più veloce per la query.

Parametro

Nuovo test con Postgres 12 su un vecchio laptop. Il mio script per generare dati fittizi:

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;

Risulta in ~ 141.000 righe generate casualmente, ~ 30.000 shop_id distinti , ~ 12.000 hours distinte . Dimensioni tabella 8 MB.

Ho eliminato e ricreato il vincolo di esclusione:

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 il primo è ~ 4 volte più veloce per questa distribuzione.

Inoltre, ne ho testati altri due per le prestazioni di lettura:

CREATE INDEX hoo_hours_gist_idx   on hoo USING gist (hours);
CREATE INDEX hoo_hours_spgist_idx on hoo USING spgist (hours);  -- !!

Dopo VACUUM FULL ANALYZE hoo; , ho eseguito due query:

  • Q1 :a tarda notte, trovando solo 35 righe
  • Q2 :nel pomeriggio, trovando 4547 righe .

Risultati

Ho ricevuto una scansione solo indice per ciascuno (tranne "senza indice", ovviamente):

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 e GiST sono alla pari per le query che trovano pochi risultati (GiST è ancora più veloce per molto pochi).
  • SP-GiST si adatta meglio con un numero crescente di risultati ed è anche più piccolo.

Se leggi molto più di quanto scrivi (caso d'uso tipico), mantieni il vincolo di esclusione come suggerito all'inizio e crea un indice SP-GiST aggiuntivo per ottimizzare le prestazioni di lettura.