PostgreSQL
 sql >> Base de données >  >> RDS >> PostgreSQL

Effectuez cette requête d'heures de fonctionnement dans PostgreSQL

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.