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:
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 souhaité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 | ==
--+---------------------------
| ====== == ===============>
Pas besoin de faire la différence entre infinity
et 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.)
Puisque date
est un type discret, toutes les plages ont par défaut [)
limites. Par documentation:
Les types de plage intégrés
int4range
,int8range
etdaterange
utilisent tous une forme canonique qui inclut la borne inférieure et exclut la borne supérieure; C'est,[)
.
Pour d'autres types (comme tsrange
!), J'appliquerais la même chose si possible:
Avec les CTE pour plus de clarté:
WITH a AS (
SELECT range
, COALESCE(lower(range),'-infinity') AS startdate
, max(COALESCE(upper(range), 'infinity')) OVER (ORDER BY range) AS enddate
FROM test
)
, b AS (
SELECT *, lag(enddate) OVER (ORDER BY range) < startdate OR NULL AS step
FROM a
)
, c AS (
SELECT *, count(step) OVER (ORDER BY range) AS grp
FROM b
)
SELECT daterange(min(startdate), max(enddate)) AS range
FROM c
GROUP BY grp
ORDER BY 1;
Ou , la même chose avec les sous-requêtes, plus rapide mais moins facile à lire aussi:
SELECT daterange(min(startdate), max(enddate)) AS range
FROM (
SELECT *, count(step) OVER (ORDER BY range) AS grp
FROM (
SELECT *, lag(enddate) OVER (ORDER BY range) < startdate OR NULL AS step
FROM (
SELECT range
, COALESCE(lower(range),'-infinity') AS startdate
, max(COALESCE(upper(range), 'infinity')) OVER (ORDER BY range) AS enddate
FROM test
) a
) b
) c
GROUP BY grp
ORDER BY 1;
Ou avec un niveau de sous-requête en moins, mais en inversant l'ordre de tri:
SELECT daterange(min(COALESCE(lower(range), '-infinity')), max(enddate)) AS range
FROM (
SELECT *, count(nextstart > enddate OR NULL) OVER (ORDER BY range DESC NULLS LAST) AS grp
FROM (
SELECT range
, max(COALESCE(upper(range), 'infinity')) OVER (ORDER BY range) AS enddate
, lead(lower(range)) OVER (ORDER BY range) As nextstart
FROM test
) a
) b
GROUP BY grp
ORDER BY 1;
ORDER BY range DESC NULLS LAST
(avec NULLS LAST
) pour obtenir parfaitement ordre de tri 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 avec rank IS NULL
. a
: lors de la commande par range
, calculez le maximum en cours d'exécution de la borne supérieure (enddate
) avec une fonction de fenêtre.
Remplacez les bornes NULL (illimitées) par +/- infinity
juste pour simplifier (pas de cas NULL spéciaux).
b
: dans le même ordre de tri, si le précédent enddate
est antérieur à startdate
we avoir un espace et commencer 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 structure externe de SELECT
, la plage s'étend de la borne inférieure à la borne supérieure de chaque groupe. Voilá.
Réponse étroitement liée le SO avec plus d'explications:
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 substantiellement plus rapide car il n'a besoin que d'un balayage séquentiel unique :
CREATE OR REPLACE FUNCTION f_range_agg(_tbl text, _col text)
RETURNS SETOF daterange AS
$func$
DECLARE
_lower date;
_upper date;
_enddate date;
_startdate date;
BEGIN
FOR _lower, _upper IN EXECUTE
format($$SELECT COALESCE(lower(t.%2$I),'-infinity') -- replace NULL with ...
, COALESCE(upper(t.%2$I), 'infinity') -- ... +/- infinity
FROM %1$I t
ORDER BY t.%2$I$$
, _tbl, _col)
LOOP
IF _lower > _enddate THEN -- return previous range
RETURN NEXT daterange(_startdate, _enddate);
SELECT _lower, _upper INTO _startdate, _enddate;
ELSIF _upper > _enddate THEN -- expand range
_enddate := _upper;
-- do nothing if _upper <= _enddate (range already included) ...
ELSIF _enddate IS NULL THEN -- init 1st round
SELECT _lower, _upper INTO _startdate, _enddate;
END IF;
END LOOP;
IF FOUND THEN -- return last row
RETURN NEXT daterange(_startdate, _enddate);
END IF;
END
$func$ LANGUAGE plpgsql;
Appel:
SELECT * FROM f_range_agg('test', 'range'); -- table and column name
La logique est similaire aux solutions SQL, mais nous pouvons nous contenter d'un seul passage.
En relation:
L'exercice habituel pour gérer les entrées utilisateur en SQL dynamique:
Pour chacune de ces solutions, un index btree simple (par défaut) sur range
serait déterminant pour les performances dans les grandes tables:
CREATE INDEX foo on test (range);
n 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 un scan d'index uniquement.
Je suis venu avec ceci:
DO $$
DECLARE
i date;
a daterange := 'empty';
day_as_range daterange;
extreme_value date := '2100-12-31';
BEGIN
FOR i IN
SELECT DISTINCT
generate_series(
lower(range),
COALESCE(upper(range) - interval '1 day', extreme_value),
interval '1 day'
)::date
FROM rangetest
ORDER BY 1
LOOP
day_as_range := daterange(i, i, '[]');
BEGIN
IF isempty(a)
THEN a := day_as_range;
ELSE a = a + day_as_range;
END IF;
EXCEPTION WHEN data_exception THEN
RAISE INFO '%', a;
a = day_as_range;
END;
END LOOP;
IF upper(a) = extreme_value + interval '1 day'
THEN a := daterange(lower(a), NULL);
END IF;
RAISE INFO '%', a;
END;
$$;
A encore besoin d'un peu de perfectionnement, mais l'idée est la suivante:
+
) échoue, retourne la plage déjà construite et réinitialiseIl y a quelques années, j'ai testé différentes solutions (entre autres certaines similaires à celles de @ErwinBrandstetter) pour fusionner les 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
de la ligne suivante en utilisant LEAD
et vous avez presque terminé. Uniquement pour la dernière ligne LEAD
renvoie un NULL
, pour résoudre ce problème, calculez la date de fin maximale de toutes les lignes d'une partition à l'étape 2 et COALESCE
celle-ci.Pourquoi c'était plus rapide? En fonction des données réelles, l'étape n ° 2 peut considérablement réduire le nombre de lignes, de sorte que l'étape suivante doit fonctionner uniquement sur un petit sous-ensemble, de plus, elle supprime l'agrégation.
SELECT
daterange(startdate
,COALESCE(LEAD(maxPrevEnddate) -- next row's end date
OVER (ORDER BY startdate)
,maxEnddate) -- or maximum end date
) AS range
FROM
(
SELECT
range
,COALESCE(LOWER(range),'-infinity') AS startdate
-- find the maximum end date of all previous rows
-- i.e. the END of the previous range
,MAX(COALESCE(UPPER(range), 'infinity'))
OVER (ORDER BY range
ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING) AS maxPrevEnddate
-- maximum end date of this partition
-- only needed for the last range
,MAX(COALESCE(UPPER(range), 'infinity'))
OVER () AS maxEnddate
FROM test
) AS dt
WHERE maxPrevEnddate < startdate -- keep the rows where a range start
OR maxPrevEnddate IS NULL -- and keep the first row
ORDER BY 1;
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.