Suposiciones / aclaraciones
No es necesario diferenciar entre infinity
y abrir el límite superior ( upper(range) IS NULL
). (Puede tenerlo de cualquier manera, pero es más simple de esta manera).
Como date
es un tipo discreto, todos los rangos tienen [)
límites predeterminados .
Por documentación:
El incorporada en tipos de rango int4range
, int8range
y daterange
todo el uso de una forma canónica que incluye el límite inferior y excluye el límite superior; es decir, [)
.
Para otros tipos (como tsrange
!) Haría cumplir lo mismo si es posible:
Solución con SQL puro
Con CTE para mayor claridad:
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;
O , lo mismo con las subconsultas, más rápido pero menos fácil de leer:
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;
O con un nivel de subconsulta menos, pero cambiando el orden de clasificación:
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;
- Ordene la ventana en el segundo paso con
ORDER BY range DESC NULLS LAST
(con NULLS LAST
) para obtener un orden de inversión perfectamente invertido. Esto debería ser más barato (más fácil de producir, coincide perfectamente con el orden de clasificación del índice sugerido) y preciso para casos de esquina rank IS NULL
.
Explique
a
: Mientras ordena range
, calcule el máximo de ejecución del límite superior ( enddate
) con una función de ventana.
Reemplace los límites NULL (sin límites) con +/- infinity
solo para simplificar (sin casos especiales NULL).
b
: En el mismo orden de clasificación, si la anterior enddate
es anterior a la startdate
que tenemos un espacio y comenzamos un nuevo rango ( step
).
Recuerde, el límite superior siempre está excluido.
c
: Forma grupos ( grp
) contando los pasos con otra función de ventana.
En la SELECT
construcción externa, el rango va desde el límite inferior al superior en cada grupo. Voilá
Respuesta estrechamente relacionada en SO con más explicación:
Solución de procedimiento con plpgsql
Funciona para cualquier nombre de tabla / columna, pero solo para el tipo daterange
.
Las soluciones de procedimiento con bucles suelen ser más lentas, pero en este caso especial espero que la función sea sustancialmente más rápida, ya que solo necesita un escaneo secuencial único :
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;
Llamada:
SELECT * FROM f_range_agg('test', 'range'); -- table and column name
La lógica es similar a las soluciones SQL, pero podemos hacerlo con un solo paso.
SQL Fiddle.
Relacionado:
El ejercicio habitual para manejar la entrada del usuario en SQL dinámico:
Índice
Para cada una de estas soluciones, un índice btree simple (predeterminado) en range
sería instrumental para el rendimiento en tablas grandes:
CREATE INDEX foo on test (range);
Un índice btree es de uso limitado para los tipos de rango , pero podemos obtener datos ordenados previamente y tal vez incluso un escaneo de solo índice.