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 JOINa las filas existentes. Relacionado: -
En su caso, es una unión de varias tablas, por lo que uso paréntesis en
FROMlista paraLEFT JOINal resultado deINNER JOINentre paréntesis. Sería incorrecto aLEFT JOINa 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
roomsyteachersen el lado izquierdo por segunda vez. Pero todavía tenemos una combinación de dos tablas (studiesyteacher_contacts). El rol deteacher_contactsno está claro para mí. Normalmente, esperaría una relación entrestudiesyteachersdirectamente. 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
teacheres difícilmente (confiablemente) único. Opere con una identificación única, preferiblemente la PK (también más rápida y sencilla). Todavía estoy usandoteacherpara 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: