J'essaie de combiner plusieurs plages de dates (ma charge est d'environ 500 max, la plupart des cas 10) qui peuvent ou non se chevaucher dans les plus grandes plages de dates contiguës possibles. Par exemple:
Les données:
CREATE TABLE test (
id SERIAL PRIMARY KEY NOT NULL,
range DATERANGE
);
INSERT INTO test (range) VALUES
(DATERANGE('2015-01-01', '2015-01-05')),
(DATERANGE('2015-01-01', '2015-01-03')),
(DATERANGE('2015-01-03', '2015-01-06')),
(DATERANGE('2015-01-07', '2015-01-09')),
(DATERANGE('2015-01-08', '2015-01-09')),
(DATERANGE('2015-01-12', NULL)),
(DATERANGE('2015-01-10', '2015-01-12')),
(DATERANGE('2015-01-10', '2015-01-12'));
Le tableau ressemble à:
id | range
----+-------------------------
1 | [2015-01-01,2015-01-05)
2 | [2015-01-01,2015-01-03)
3 | [2015-01-03,2015-01-06)
4 | [2015-01-07,2015-01-09)
5 | [2015-01-08,2015-01-09)
6 | [2015-01-12,)
7 | [2015-01-10,2015-01-12)
8 | [2015-01-10,2015-01-12)
(8 rows)
Résultats désirés:
combined
--------------------------
[2015-01-01, 2015-01-06)
[2015-01-07, 2015-01-09)
[2015-01-10, )
Représentation visuelle:
1 | =====
2 | ===
3 | ===
4 | ==
5 | =
6 | =============>
7 | ==
8 | ==
--+---------------------------
| ====== == ===============>
postgresql
aggregate
range-types
Villiers Strauss
la source
la source
Réponses:
Hypothèses / clarifications
Pas besoin de différencier entre
infinity
et d'ouvrir la limite supérieure (upper(range) IS NULL
). (Vous pouvez l'avoir de toute façon, mais c'est plus simple de cette façon.)infinity
dans les types de plage PostgreSQLPuisque
date
c'est un type discret, toutes les plages ont des[)
limites par défaut . Par documentation:Pour d'autres types (comme
tsrange
!) J'appliquerais la même chose si possible:Solution avec SQL pur
Avec les CTE pour plus de clarté:
Ou , la même chose avec les sous-requêtes, plus rapide mais moins facile à lire aussi:
Ou avec un niveau de sous-requête en moins, mais en inversant l'ordre de tri:
ORDER BY range DESC NULLS LAST
(avecNULLS LAST
) pour obtenir un ordre de tri parfaitement inversé. Cela devrait être moins cher (plus facile à produire, correspond parfaitement à l'ordre de tri de l'index suggéré) et précis pour les cas d'angle avecrank IS NULL
.Explique
a
: Lors de la commande parrange
, calculez le maximum courant de la limite supérieure (enddate
) avec une fonction de fenêtre.Remplacez les limites NULL (sans limite) par +/-
infinity
juste pour simplifier (pas de cas NULL spéciaux).b
: Dans le même ordre de tri, si le précédentenddate
est antérieur,startdate
nous avons un écart et commençons une nouvelle plage (step
).N'oubliez pas que la limite supérieure est toujours exclue.
c
: Formez des groupes (grp
) en comptant les étapes avec une autre fonction de fenêtre.Dans la
SELECT
construction externe s'étend de la limite inférieure à la limite supérieure dans chaque groupe. Voilá.Réponse étroitement liée à SO avec plus d'explications:
Solution procédurale avec plpgsql
Fonctionne pour n'importe quel nom de table / colonne, mais uniquement pour le type
daterange
.Les solutions procédurales avec boucles sont généralement plus lentes, mais dans ce cas particulier, je m'attends à ce que la fonction soit sensiblement plus rapide car elle n'a besoin que d'un seul balayage séquentiel :
Appel:
La logique est similaire aux solutions SQL, mais nous pouvons nous contenter d'un seul passage.
SQL Fiddle.
En relation:
L'exercice habituel pour gérer les entrées utilisateur en SQL dynamique:
Indice
Pour chacune de ces solutions, un index btree simple (par défaut)
range
serait déterminant pour les performances dans les grandes tables:Un index btree est d'une utilité limitée pour les types de plage , mais nous pouvons obtenir des données pré-triées et peut-être même une analyse d'index uniquement.
la source
EXPLAIN ( ANALYZE, TIMING OFF)
et comparer le meilleur des cinq.max(COALESCE(upper(range), 'infinity')) OVER (ORDER BY range) AS enddate
sert le (CTE A) ? Ça ne peut pas être justeCOALESCE(upper(range), 'infinity') as enddate
? AFAIKmax() + over (order by range)
reviendra justeupper(range)
ici.Je suis venu avec ceci:
A encore besoin d'un peu de perfectionnement, mais l'idée est la suivante:
+
) échoue, retourne la plage déjà construite et réinitialisela source
generate_series()
pour chaque ligne, surtout s'il peut y avoir des plages ouvertes ...Il y a quelques années, j'ai testé différentes solutions (parmi d'autres similaires à celles de @ErwinBrandstetter) pour fusionner des périodes qui se chevauchent sur un système Teradata et j'ai trouvé la suivante la plus efficace (en utilisant les fonctions analytiques, une version plus récente de Teradata a des fonctions intégrées pour cette tâche).
maxEnddate
maxEnddate
aideLEAD
et vous avez presque terminé. Uniquement pour la dernière ligneLEAD
renvoie unNULL
, pour résoudre ce problème, calculez la date de fin maximale de toutes les lignes d'une partition à l'étape 2 etCOALESCE
ainsi de suite.Pourquoi c'était plus rapide? En fonction des données réelles, l'étape # 2 pourrait réduire considérablement le nombre de lignes, de sorte que l'étape suivante doit fonctionner uniquement sur un petit sous-ensemble, en outre, elle supprime l'agrégation.
violon
Comme c'était le plus rapide sur Teradata, je ne sais pas si c'est la même chose pour PostgreSQL, ce serait bien d'obtenir des chiffres de performances réels.
la source
Pour le plaisir, je lui ai donné un coup de feu. J'ai trouvé que c'était la méthode la plus rapide et la plus propre pour ce faire. D'abord, nous définissons une fonction qui fusionne s'il y a un chevauchement ou si les deux entrées sont adjacentes, s'il n'y a pas de chevauchement ou d'adjacence, nous renvoyons simplement la première daterange. Hint
+
est une union de plage dans le contexte des plages.Ensuite, nous l'utilisons comme ça,
la source
('2015-01-01', '2015-01-03'), ('2015-01-03', '2015-01-05'), ('2015-01-05', '2015-01-06')
.