Estoy tratando de combinar varios rangos de fechas (mi carga es de aproximadamente 500, la mayoría de los casos 10) que pueden superponerse o no en los rangos de fechas contiguas más grandes posibles. Por ejemplo:
Datos:
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'));
La mesa se ve así:
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)
Resultados deseados:
combined
--------------------------
[2015-01-01, 2015-01-06)
[2015-01-07, 2015-01-09)
[2015-01-10, )
Representación visual:
1 | =====
2 | ===
3 | ===
4 | ==
5 | =
6 | =============>
7 | ==
8 | ==
--+---------------------------
| ====== == ===============>
postgresql
aggregate
range-types
Villiers Strauss
fuente
fuente
Respuestas:
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).infinity
en tipos de rango PostgreSQLComo
date
es un tipo discreto, todos los rangos tienen[)
límites predeterminados . Por documentación:Para otros tipos (como
tsrange
!) Haría cumplir lo mismo si es posible:Solución con SQL puro
Con CTE para mayor claridad:
O , lo mismo con las subconsultas, más rápido pero menos fácil de leer:
O con un nivel de subconsulta menos, pero cambiando el orden de clasificación:
ORDER BY range DESC NULLS LAST
(conNULLS 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 esquinarank IS NULL
.Explique
a
: Mientras ordenarange
, 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 anteriorenddate
es anterior a lastartdate
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 :
Llamada:
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: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.
fuente
EXPLAIN ( ANALYZE, TIMING OFF)
y compara el mejor de cinco.max(COALESCE(upper(range), 'infinity')) OVER (ORDER BY range) AS enddate
sirve (CTE A) . ¿No puede ser justoCOALESCE(upper(range), 'infinity') as enddate
? AFAIKmax() + over (order by range)
regresará justoupper(range)
aquí.Se me ocurrió esto:
Todavía necesita un poco de perfeccionamiento, pero la idea es la siguiente:
+
) falla, devuelve el rango ya construido y reinicializafuente
generate_series()
por cada fila, especialmente si puede haber rangos abiertos ...Hace algunos años probé diferentes soluciones (entre otras, algunas similares a las de @ErwinBrandstetter) para fusionar períodos superpuestos en un sistema Teradata y encontré la siguiente más eficiente (usando funciones analíticas, la versión más nueva de Teradata tiene funciones integradas para esa tarea).
maxEnddate
maxEnddate
usa la siguiente filaLEAD
y ya casi está listo. Solo para la última filaLEAD
devuelve aNULL
, para resolver esto, calcule la fecha de finalización máxima de todas las filas de una partición en el paso 2 yCOALESCE
luego.¿Por qué fue más rápido? Dependiendo de los datos reales, el paso n. ° 2 podría reducir en gran medida el número de filas, por lo que el siguiente paso solo debe funcionar en un pequeño subconjunto, además elimina la agregación.
violín
Como esto fue más rápido en Teradata, no sé si es lo mismo para PostgreSQL, sería bueno obtener algunos números de rendimiento reales.
fuente
Por diversión, lo probé. Encontré que este es el método más rápido y limpio para hacer esto. Primero definimos una función que se fusiona si hay una superposición o si las dos entradas son adyacentes, si no hay superposición o adyacencia, simplemente devolvemos el primer rango de fechas. Sugerencia
+
es una unión de rango en el contexto de rangos.Luego lo usamos así,
fuente
('2015-01-01', '2015-01-03'), ('2015-01-03', '2015-01-05'), ('2015-01-05', '2015-01-06')
.