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.