Mise en page du tableau
Reconcevoir le tableau pour stocker les heures d'ouverture (heures d'ouverture) sous la forme d'un ensemble de tsrange
(plage de timestamp without time zone
) valeurs. Nécessite Postgres 9.2 ou version ultérieure .
Choisissez une semaine au hasard pour organiser vos heures d'ouverture. J'aime la semaine :
1996-01-01 (lundi) au 1996-01-07 (dimanche)
Il s'agit de l'année bissextile la plus récente où le 1er janvier se trouve être un lundi. Mais cela peut être n'importe quelle semaine aléatoire pour ce cas. Soyez juste cohérent.
Installez le module supplémentaire btree_gist
d'abord :
CREATE EXTENSION btree_gist;
Voir :
- Équivalent à la contrainte d'exclusion composée d'un entier et d'une plage
Créez ensuite le tableau comme ceci :
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]')
);
Le celui colonne hours
remplace toutes vos colonnes :
opens_on, closes_on, opens_at, closes_at
Par exemple, heures d'ouverture à partir du mercredi, 18h30 au jeudi, 05:00 UTC sont entrés comme :
'[1996-01-03 18:30, 1996-01-04 05:00]'
La contrainte d'exclusion hoo_no_overlap
empêche le chevauchement des entrées par magasin. Il est implémenté avec un index GiST , qui prend également en charge nos requêtes. Consultez le chapitre "Indice et performances" ci-dessous pour discuter des stratégies d'indexation.
La contrainte de vérification hoo_bounds_inclusive
applique des limites inclusives pour vos plages, avec deux conséquences notables :
- Un point dans le temps tombant exactement sur la limite inférieure ou supérieure est toujours inclus.
- Les entrées adjacentes pour le même magasin sont effectivement interdites. Avec des limites inclusives, celles-ci se "chevaucheraient" et la contrainte d'exclusion lèverait une exception. Les entrées adjacentes doivent être fusionnées en une seule ligne à la place. Sauf quand ils se terminent vers minuit le dimanche , auquel cas ils doivent être divisés en deux lignes. La fonction
f_hoo_hours()
ci-dessous s'en charge.
La contrainte de vérification hoo_standard_week
applique les limites extérieures de la semaine intermédiaire à l'aide de l'opérateur "la plage est contenue par" <@
.
Avec inclusif limites, vous devez observer un cas d'angle où l'heure s'achève le dimanche à minuit :
'1996-01-01 00:00+0' = '1996-01-08 00:00+0'
Mon 00:00 = Sun 24:00 (= next Mon 00:00)
Vous devez rechercher les deux horodatages à la fois. Voici un cas connexe avec exclusif limite supérieure qui ne présenterait pas cette lacune :
- Éviter les entrées adjacentes/qui se chevauchent avec EXCLUDE dans PostgreSQL
Fonction f_hoo_time(timestamptz)
Pour "normaliser" n'importe quel 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
uniquement pour Postgres 9.6 ou version ultérieure.
La fonction prend timestamptz
et renvoie timestamp
. Il ajoute l'intervalle écoulé de la semaine respective ($1 - date_trunc('week', $1)
en heure UTC jusqu'au point de départ de notre semaine de préparation. (date
+ interval
produit timestamp
.)
Fonction f_hoo_hours(timestamptz, timestamptz)
Pour normaliser les plages et diviser celles qui traversent le lundi 00:00. Cette fonction prend n'importe quel intervalle (comme deux timestamptz
) et produit un ou deux tsrange
normalisés valeurs. Il couvre tous entrée légale et interdit le reste :
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$;
Pour INSERT
un célibataire ligne d'entrée :
INSERT INTO hoo(shop_id, hours)
SELECT 123, f_hoo_hours('2016-01-11 00:00+04', '2016-01-11 08:00+04');
Pour tous nombre de lignes d'entrée :
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);
Chacun peut insérer deux lignes si une plage doit être fractionnée au lundi 00:00 UTC.
Requête
Avec la conception ajustée, toute votre requête importante, complexe et coûteuse peut être remplacé par ... ceci :
SELECT *
FROM hoo
WHERE hours @> f_hoo_time(now());
Pour un peu de suspense, j'ai mis une plaque de spoiler sur la solution. Déplacez la souris sur ça.
La requête est soutenue par ledit index GiST et rapide, même pour les grandes tables.
db<>jouez ici (avec plus d'exemples)
Ancien sqlfiddle
Si vous souhaitez calculer le nombre total d'heures d'ouverture (par magasin), voici une recette :
- Calculer les heures de travail entre 2 dates dans PostgreSQL
Indice et performances
L'opérateur de confinement pour les types de plage peut être pris en charge avec un GiST ou SP-GiST indice. L'un ou l'autre peut être utilisé pour implémenter une contrainte d'exclusion, mais seul GiST prend en charge les index multicolonnes :
Actuellement, seuls les types d'index B-tree, GiST, GIN et BRIN prennent en charge les index multicolonnes.
Et l'ordre des colonnes d'index est important :
Un index GiST multicolonne peut être utilisé avec des conditions de requête qui impliquent n'importe quel sous-ensemble des colonnes de l'index. Les conditions sur les colonnes supplémentaires restreignent les entrées renvoyées par l'index, mais la condition sur la première colonne est la plus importante pour déterminer la quantité d'index à analyser. Un index GiST sera relativement inefficace si sa première colonne n'a que quelques valeurs distinctes, même s'il y a de nombreuses valeurs distinctes dans des colonnes supplémentaires.
Nous avons donc des intérêts conflictuels ici. Pour les grandes tables, il y aura beaucoup plus de valeurs distinctes pour shop_id
que pendant hours
.
- Un index GiST avec
shop_id
en tête est plus rapide à écrire et à appliquer la contrainte d'exclusion. - Mais nous cherchons
hours
dans notre requête. Il serait préférable d'avoir cette colonne en premier. - Si nous devons rechercher
shop_id
dans d'autres requêtes, un index btree simple est beaucoup plus rapide pour cela. - Pour couronner le tout, j'ai trouvé un SP-GiST index sur seulement
hours
être le plus rapide pour la requête.
Référence
Nouveau test avec Postgres 12 sur un vieux portable.Mon script pour générer des données factices :
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;
Résultats dans ~ 141 000 lignes générées aléatoirement, ~ 30 000 shop_id
distincts , ~ 12 000 hours
distinctes . Taille du tableau 8 Mo.
J'ai supprimé et recréé la contrainte d'exclusion :
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
first est ~ 4x plus rapide pour cette distribution.
De plus, j'en ai testé deux autres pour les performances de lecture :
CREATE INDEX hoo_hours_gist_idx on hoo USING gist (hours);
CREATE INDEX hoo_hours_spgist_idx on hoo USING spgist (hours); -- !!
Après VACUUM FULL ANALYZE hoo;
, j'ai exécuté deux requêtes :
- T1 :tard dans la nuit, ne trouvant que 35 lignes
- T2 :dans l'après-midi, recherche de 4547 lignes .
Résultats
Vous avez une analyse d'index uniquement pour chacun (sauf pour "pas d'index", bien sûr) :
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 et GiST sont à égalité pour les requêtes qui trouvent peu de résultats (GiST est encore plus rapide pour très peu).
- SP-GiST évolue mieux avec un nombre croissant de résultats, et est également plus petit.
Si vous lisez beaucoup plus que vous n'écrivez (cas d'utilisation typique), conservez la contrainte d'exclusion comme suggéré au départ et créez un index SP-GiST supplémentaire pour optimiser les performances de lecture.