Postgres: ¿cómo devolver filas con recuento 0 para datos faltantes?
-
19-08-2019 - |
Pregunta
He distribuido datos de manera desigual (fecha wrt) durante algunos años (2003-2008). Quiero consultar datos para un conjunto determinado de fechas de inicio y finalización, agrupando los datos por cualquiera de los intervalos admitidos (día, semana, mes, trimestre, año) en PostgreSQL 8.3 ( http://www.postgresql.org/docs/8.3/static/functions-datetime.html # FUNCTIONS-DATETIME-TRUNC ).
El problema es que algunas de las consultas dan resultados continuos durante el período requerido, como este:
select to_char(date_trunc('month',date), 'YYYY-MM-DD'),count(distinct post_id)
from some_table where category_id=1 and entity_id = 77 and entity2_id = 115
and date <= '2008-12-06' and date >= '2007-12-01' group by
date_trunc('month',date) order by date_trunc('month',date);
to_char | count
------------+-------
2007-12-01 | 64
2008-01-01 | 31
2008-02-01 | 14
2008-03-01 | 21
2008-04-01 | 28
2008-05-01 | 44
2008-06-01 | 100
2008-07-01 | 72
2008-08-01 | 91
2008-09-01 | 92
2008-10-01 | 79
2008-11-01 | 65
(12 rows)
pero algunos pierden algunos intervalos porque no hay datos presentes, como este:
select to_char(date_trunc('month',date), 'YYYY-MM-DD'),count(distinct post_id)
from some_table where category_id=1 and entity_id = 75 and entity2_id = 115
and date <= '2008-12-06' and date >= '2007-12-01' group by
date_trunc('month',date) order by date_trunc('month',date);
to_char | count
------------+-------
2007-12-01 | 2
2008-01-01 | 2
2008-03-01 | 1
2008-04-01 | 2
2008-06-01 | 1
2008-08-01 | 3
2008-10-01 | 2
(7 rows)
donde el conjunto de resultados requerido es:
to_char | count
------------+-------
2007-12-01 | 2
2008-01-01 | 2
2008-02-01 | 0
2008-03-01 | 1
2008-04-01 | 2
2008-05-01 | 0
2008-06-01 | 1
2008-07-01 | 0
2008-08-01 | 3
2008-09-01 | 0
2008-10-01 | 2
2008-11-01 | 0
(12 rows)
Un recuento de 0 para entradas faltantes.
He visto discusiones anteriores sobre Stack Overflow pero parece que no resuelven mi problema, ya que mi período de agrupación es uno (día, semana, mes, trimestre, año) y la aplicación decidió el tiempo de ejecución. Entonces, un enfoque como la combinación izquierda con una tabla de calendario o una tabla de secuencia no ayudará, supongo.
Mi solución actual para esto es llenar estos vacíos en Python (en una aplicación Turbogears) usando el módulo de calendario.
¿Hay una mejor manera de hacer esto?
Solución
Puede crear la lista de todos los primeros días del último año (digamos) con
select distinct date_trunc('month', (current_date - offs)) as date
from generate_series(0,365,28) as offs;
date
------------------------
2007-12-01 00:00:00+01
2008-01-01 00:00:00+01
2008-02-01 00:00:00+01
2008-03-01 00:00:00+01
2008-04-01 00:00:00+02
2008-05-01 00:00:00+02
2008-06-01 00:00:00+02
2008-07-01 00:00:00+02
2008-08-01 00:00:00+02
2008-09-01 00:00:00+02
2008-10-01 00:00:00+02
2008-11-01 00:00:00+01
2008-12-01 00:00:00+01
Entonces puedes unirte a esa serie.
Otros consejos
Esta pregunta es antigua. Pero como otros usuarios lo eligieron como maestro para un nuevo duplicado, estoy agregando una respuesta adecuada.
Solución adecuada
SELECT *
FROM (
SELECT day::date
FROM generate_series(timestamp '2007-12-01'
, timestamp '2008-12-01'
, interval '1 month') day
) d
LEFT JOIN (
SELECT date_trunc('month', date_col)::date AS day
, count(*) AS some_count
FROM tbl
WHERE date_col >= date '2007-12-01'
AND date_col <= date '2008-12-06'
-- AND ... more conditions
GROUP BY 1
) t USING (day)
ORDER BY day;
-
Use
LEFT JOIN
, por supuesto. -
generate_series ()
puede producir una tabla de marcas de tiempo sobre la marcha, y muy rápido. -
Generalmente es más rápido agregar antes de unirse. Recientemente proporcioné un caso de prueba en sqlfiddle.com en esta respuesta relacionada:
-
Transmita la marca de tiempo
date
(:: date
) para obtener un formato básico. Para más uso,to_char ()
. GROUP BY 1
es una sintaxis abreviada para hacer referencia a la primera columna de salida. También podría serGROUP BY day
, pero eso podría entrar en conflicto con una columna existente del mismo nombre. OGROUP BY date_trunc ('mes', date_col) :: date
pero eso es demasiado largo para mi gusto.-
Funciona con los argumentos de intervalo disponibles para
date_trunc ()
. -
count ()
nunca produceNULL
(0
para ninguna fila), pero elLEFT JOIN
sí.
Para devolver0
en lugar deNULL
en elSELECT
externo, useCOALESCE (some_count, 0) AS some_count
. El manual. -
Para una solución más genérica o intervalos de tiempo arbitrarios considere esta respuesta estrechamente relacionada:
Podría crear una tabla temporal en tiempo de ejecución y dejar unirse en eso. Eso parece tener más sentido.