Hypothèses / clarifications
Pas besoin de différencier entre infinityet 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.)
Puisque datec'est un type discret, toutes les plages ont des [)limites par défaut .
Par documentation:
Le haut-types de plage int4range, int8rangeet daterangetoute utilisation d' une forme canonique qui comprend la borne inférieure et la borne supérieure ne comprend pas; c'est-à-dire [),.
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é:
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;
- Triez la fenêtre dans la deuxième étape avec
ORDER BY range DESC NULLS LAST(avec NULLS 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 avec rank IS NULL.
Explique
a: Lors de la commande par range, calculez le maximum courant de la limite supérieure ( enddate) avec une fonction de fenêtre.
Remplacez les limites NULL (sans limite) par +/- infinityjuste pour simplifier (pas de cas NULL spéciaux).
b: Dans le même ordre de tri, si le précédent enddateest antérieur, startdatenous 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 SELECTconstruction 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 :
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.
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) rangeserait déterminant pour les performances dans les grandes tables:
CREATE INDEX foo on test (range);
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.