Tengo una tabla (en PostgreSQL 9.4) que se ve así:
CREATE TABLE dates_ranges (kind int, start_date date, end_date date);
INSERT INTO dates_ranges VALUES
(1, '2018-01-01', '2018-01-31'),
(1, '2018-01-01', '2018-01-05'),
(1, '2018-01-03', '2018-01-06'),
(2, '2018-01-01', '2018-01-01'),
(2, '2018-01-01', '2018-01-02'),
(3, '2018-01-02', '2018-01-08'),
(3, '2018-01-05', '2018-01-10');
Ahora quiero calcular para las fechas dadas y para cada tipo, en cuántas filas de dates_ranges
cada fecha cae. Los ceros podrían posiblemente omitirse.
Resultado deseado:
+-------+------------+----+
| kind | as_of_date | n |
+-------+------------+----+
| 1 | 2018-01-01 | 2 |
| 1 | 2018-01-02 | 2 |
| 1 | 2018-01-03 | 3 |
| 2 | 2018-01-01 | 2 |
| 2 | 2018-01-02 | 1 |
| 3 | 2018-01-02 | 1 |
| 3 | 2018-01-03 | 1 |
+-------+------------+----+
Se me ocurrieron dos soluciones, una con LEFT JOIN
yGROUP BY
SELECT
kind, as_of_date, COUNT(*) n
FROM
(SELECT d::date AS as_of_date FROM generate_series('2018-01-01'::timestamp, '2018-01-03'::timestamp, '1 day') d) dates
LEFT JOIN
dates_ranges ON dates.as_of_date BETWEEN start_date AND end_date
GROUP BY 1,2 ORDER BY 1,2
y uno con LATERAL
, que es un poco más rápido:
SELECT
kind, as_of_date, n
FROM
(SELECT d::date AS as_of_date FROM generate_series('2018-01-01'::timestamp, '2018-01-03'::timestamp, '1 day') d) dates,
LATERAL
(SELECT kind, COUNT(*) AS n FROM dates_ranges WHERE dates.as_of_date BETWEEN start_date AND end_date GROUP BY kind) ss
ORDER BY kind, as_of_date
Me pregunto si hay alguna forma mejor de escribir esta consulta. ¿Y cómo incluir pares date-kind con 0 count?
En realidad, hay algunos tipos distintos, un período de hasta cinco años (1800 fechas) y ~ 30k filas en la dates_ranges
tabla (pero podría crecer significativamente).
No hay índices Para ser precisos en mi caso, es el resultado de una subconsulta, pero he querido limitar la pregunta a un problema, por lo que es más general.
fuente
(1,2018-01-01,2018-01-15)
y(1,2018-01-20,2018-01-25)
desea tenerlo en cuenta al determinar cuántas fechas superpuestas tiene?2018-01-31
o2018-01-30
o2018-01-29
en él cuando la primera gama cuenta con todos ellos?generate_series
son parámetros externos; no necesariamente cubren todos los rangos de ladates_ranges
tabla. En cuanto a la primera pregunta, supongo que no la entiendo: las filasdates_ranges
son independientes, no quiero determinar la superposición.Respuestas:
La siguiente consulta también funciona si los "ceros faltantes" están bien:
pero no es más rápido que la
lateral
versión con el pequeño conjunto de datos. Sin embargo, podría escalar mejor, ya que no se requiere unión, pero la versión anterior se agrega a todas las filas, por lo que puede perder nuevamente.La siguiente consulta intenta evitar el trabajo innecesario eliminando cualquier serie que no se superponga de todos modos:
- ¡Y tengo que usar el
overlaps
operador! Tenga en cuenta que debe agregarinterval '1 day'
a la derecha ya que el operador de superposición considera que los períodos de tiempo están abiertos a la derecha (lo cual es bastante lógico porque a menudo se considera que una fecha es una marca de tiempo con un componente de hora de medianoche).fuente
generate_series
se puede usar así. Después de algunas pruebas tengo las siguientes observaciones. De hecho, su consulta se escala muy bien con la longitud del rango seleccionado; prácticamente no hay diferencia entre un período de 3 años y 10 años. Sin embargo, durante períodos más cortos (1 año), mis soluciones son más rápidas. Supongo que la razón es que hay algunos rangos realmente largosdates_ranges
(como 2010-2100), que están ralentizando su consulta. Sin embargo, limitarstart_date
yend_date
dentro de la consulta interna debería ayudar. Necesito hacer algunas pruebas más.Construya una cuadrícula de todas las combinaciones y luego
LATERAL
únase a su mesa, así:También debe ser lo más rápido posible.
Al
LEFT JOIN LATERAL ... on true
principio tenía , pero hay un agregado en la subconsultac
, por lo que siempre obtenemos una fila y también podemos usarlaCROSS JOIN
. No hay diferencia en el rendimiento.Si tiene una tabla que contiene todos los tipos relevantes , úsela en lugar de generar la lista con subconsulta
k
.El reparto a
integer
es opcional. De lo contrario tienesbigint
.Los índices ayudarían, especialmente un índice de varias columnas en
(kind, start_date, end_date)
. Como está construyendo sobre una subconsulta, esto puede o no ser posible de lograr.El uso de funciones de devolución de conjuntos como
generate_series()
en laSELECT
lista generalmente no es aconsejable en las versiones de Postgres anteriores a la 10 (a menos que sepa exactamente lo que está haciendo). Ver:Si tiene muchas combinaciones con pocas o ninguna fila, esta forma equivalente puede ser más rápida:
fuente
SELECT
lista, he leído que no es recomendable, sin embargo, parece que funciona bien, si solo hay una de esas funciones. Si estoy seguro de que solo habrá uno, ¿podría salir algo mal?SELECT
lista funciona como se esperaba. Tal vez agregue un comentario para advertir en contra de agregar otro. O muévalo a laFROM
lista para comenzar en versiones anteriores de Postgres. ¿Por qué arriesgar complicaciones? (Eso también es SQL estándar y no confundirá a las personas que vienen de otros RDBMS).Usando el
daterange
tipoPostgreSQL tiene un
daterange
. Usarlo es bastante simple. Comenzando con sus datos de muestra, nos movemos para usar el tipo en la tabla.Ahora, para consultarlo, revertimos el procedimiento y generamos una serie de fechas, pero aquí está el problema de que la consulta en sí misma puede usar el
@>
operador de contención ( ) para verificar que las fechas estén dentro del rango, usando un índice.Tenga en cuenta que usamos
timestamp without time zone
(para detener los riesgos de DST)Cuál es la superposición de día detallada en el índice.
Como beneficio adicional, con el tipo de rango de fechas puede detener las inserciones de rangos que se superponen con otros utilizando un
EXCLUDE CONSTRAINT
fuente
JOIN
demasiado, supongo.count(DISTINCT kind)
1
fecha de tipo2018-01-01
está dentro de las dos primeras filas desdedates_ranges
, pero su consulta proporciona8
.count(DISTINCT kind)
¿agregó laDISTINCT
palabra clave allí?DISTINCT
palabra clave todavía no funciona como se esperaba. Cuenta distintos tipos para cada fecha, pero quiero contar todas las filas de cada tipo para cada fecha.