Com base em algumas suposições (ambiguidades na pergunta) sugiro:
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;
Pontos principais
-
Construa uma grade de todas as combinações desejadas comCROSS JOIN
. E entãoLEFT JOIN
às linhas existentes. Relacionado:
-
No seu caso, é uma junção de várias tabelas, então eu uso parênteses noFROM
list paraLEFT JOIN
para o resultado deINNER JOIN
entre parênteses. Seria incorreto paraLEFT JOIN
para cada tabela separadamente, porque você incluiria acertos em correspondências parciais e obteria contagens potencialmente incorretas.
-
Assumindo integridade referencial e trabalhando diretamente com colunas PK, não precisamos incluirrooms
eteachers
no lado esquerdo uma segunda vez. Mas ainda temos uma junção de duas tabelas (studies
eteacher_contacts
). A função deteacher_contacts
não está claro para mim. Normalmente, eu esperaria uma relação entrestudies
eteachers
diretamente. Poderia ser mais simplificado...
-
Precisamos contar uma coluna não nula no lado esquerdo para obter as contagens desejadas. Comocount(s.room_id)
-
Para manter isso rápido para tabelas grandes, certifique-se de que seus predicados sejam sargable . E adicione índices correspondentes .
-
A colunateacher
é dificilmente (confiavelmente) único. Opere com um ID único, preferencialmente o PK (mais rápido e simples também). Ainda estou usando oteacher
para que a saída corresponda ao resultado desejado. Pode ser aconselhável incluir um ID exclusivo, pois os nomes podem ser duplicados.
-
Você quer:
Então comece comdate_trunc('month', now() - interval '12 month'
(não 13). Isso já está arredondando o início e faz o que você deseja - com mais precisão do que sua consulta original.
Como você mencionou desempenho lento, dependendo das definições reais da tabela e da distribuição de dados, provavelmente é mais rápido agregar primeiro e juntar depois , como nesta resposta 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;
Sobre sua observação de encerramento:
Provavelmente, você pode use a forma de dois parâmetros de
crosstab()
para produzir o resultado desejado diretamente e com excelente desempenho e a consulta acima não é necessária para começar. Considerar: