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.)
Puisque date
c'est un type discret, toutes les plages ont des [)
limites par défaut .
Par documentation:
Le haut-types de plage int4range
, int8range
et daterange
toute 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 +/- 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
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 :
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) range
serait 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.