Basado en algunas suposiciones (ambigüedades en la pregunta), sugiero:
SELECT upper(trim(t.full_name)) AS teacher
, m.study_month
, r.room_code AS room
, count(s.room_id) AS study_count
FROM teachers t
CROSS JOIN generate_series(date_trunc('month', now() - interval '12 month') -- 12!
, date_trunc('month', now())
, interval '1 month') m(study_month)
CROSS JOIN rooms r
LEFT JOIN ( -- parentheses!
studies s
JOIN teacher_contacts tc ON tc.id = s.teacher_contact_id -- INNER JOIN!
) ON tc.teacher_id = t.id
AND s.study_dt >= m.study_month
AND s.study_dt < m.study_month + interval '1 month' -- sargable!
AND s.room_id = r.id
GROUP BY t.id, m.study_month, r.id -- id is PK of respective tables
ORDER BY t.id, m.study_month, r.id;
Puntos principales
-
Cree una cuadrícula de todas las combinaciones deseadas con
CROSS JOIN
. Y luegoLEFT JOIN
a las filas existentes. Relacionado: -
En su caso, es una unión de varias tablas, por lo que uso paréntesis en
FROM
lista paraLEFT JOIN
al resultado deINNER JOIN
entre paréntesis. Sería incorrecto aLEFT JOIN
a cada tabla por separado, porque incluiría aciertos en coincidencias parciales y obtendría recuentos potencialmente incorrectos. -
Asumiendo integridad referencial y trabajando con columnas PK directamente, no necesitamos incluir
rooms
yteachers
en el lado izquierdo por segunda vez. Pero todavía tenemos una combinación de dos tablas (studies
yteacher_contacts
). El rol deteacher_contacts
no está claro para mí. Normalmente, esperaría una relación entrestudies
yteachers
directamente. Podría simplificarse aún más... -
Necesitamos contar una columna no nula en el lado izquierdo para obtener los recuentos deseados. Me gusta
count(s.room_id)
-
Para mantener esto rápido para tablas grandes, asegúrese de que sus predicados sean sargable . Y agregue índices coincidentes .
-
La columna
teacher
es difícilmente (confiablemente) único. Opere con una identificación única, preferiblemente la PK (también más rápida y sencilla). Todavía estoy usandoteacher
para que la salida coincida con el resultado deseado. Puede ser conveniente incluir una identificación única, ya que los nombres pueden estar duplicados. -
Quieres:
Así que empieza con
date_trunc('month', now() - interval '12 month'
(no 13). Eso ya está redondeando hacia abajo el inicio y hace lo que quiere, con más precisión que su consulta original.
Dado que mencionó un rendimiento lento, dependiendo de las definiciones de tabla reales y la distribución de datos, probablemente sea más rápido agregar primero y unirse más tarde , como en esta respuesta relacionada:
SELECT upper(trim(t.full_name)) AS teacher
, m.mon AS study_month
, r.room_code AS room
, COALESCE(s.ct, 0) AS study_count
FROM teachers t
CROSS JOIN generate_series(date_trunc('month', now() - interval '12 month') -- 12!
, date_trunc('month', now())
, interval '1 month') mon
CROSS JOIN rooms r
LEFT JOIN ( -- parentheses!
SELECT tc.teacher_id, date_trunc('month', s.study_dt) AS mon, s.room_id, count(*) AS ct
FROM studies s
JOIN teacher_contacts tc ON s.teacher_contact_id = tc.id
WHERE s.study_dt >= date_trunc('month', now() - interval '12 month') -- sargable
GROUP BY 1, 2, 3
) s ON s.teacher_id = t.id
AND s.mon = m.mon
AND s.room_id = r.id
ORDER BY 1, 2, 3;
Acerca de su comentario de cierre:
Lo más probable es que puedas use la forma de dos parámetros de crosstab()
para producir el resultado deseado directamente y con un rendimiento excelente y la consulta anterior no es necesaria para empezar. Considere: