HAVING: filtrando grupos
1. Introdução ao HAVING
No SQL, a filtragem de dados pode ocorrer em diferentes momentos da execução de uma consulta. Enquanto a cláusula WHERE filtra linhas individuais antes da agregação, o HAVING atua após o agrupamento, permitindo filtrar grupos inteiros com base em resultados de funções de agregação.
A diferença fundamental é: WHERE não pode usar funções de agregação como COUNT(), SUM(), AVG() etc., porque essas funções só são calculadas após o GROUP BY. O HAVING foi criado exatamente para preencher essa lacuna.
A ordem de execução no SQL é:
1. FROM / JOIN
2. WHERE
3. GROUP BY
4. HAVING
5. SELECT
6. ORDER BY
Isso significa que o HAVING vê os dados já agrupados e agregados, mas ainda antes da projeção final do SELECT.
2. Sintaxe Básica do HAVING
A estrutura completa de uma consulta com HAVING é:
SELECT coluna_agrupada, funcao_agregacao
FROM tabela
WHERE condicao_individual
GROUP BY coluna_agrupada
HAVING condicao_do_grupo;
Exemplo simples: encontrar categorias de produtos com mais de 5 itens cadastrados.
SELECT categoria, COUNT(*) AS total_produtos
FROM produtos
GROUP BY categoria
HAVING COUNT(*) > 5;
Esse comando retorna apenas as categorias cuja contagem de produtos ultrapassa 5.
3. HAVING com Funções de Agregação
COUNT: grupos com mais de N registros
SELECT departamento, COUNT(*) AS qtd_funcionarios
FROM funcionarios
GROUP BY departamento
HAVING COUNT(*) >= 10;
SUM: grupos com soma acima de um limite
SELECT cliente_id, SUM(valor_total) AS total_gasto
FROM pedidos
GROUP BY cliente_id
HAVING SUM(valor_total) > 5000;
AVG: média dentro de um intervalo
SELECT turma, AVG(nota_final) AS media_turma
FROM alunos
GROUP BY turma
HAVING AVG(nota_final) BETWEEN 7 AND 10;
MIN e MAX: filtrando por valores extremos
SELECT produto_id, MIN(preco) AS menor_preco, MAX(preco) AS maior_preco
FROM precos_historicos
GROUP BY produto_id
HAVING MAX(preco) - MIN(preco) > 100;
4. HAVING com Múltiplas Condições
Assim como no WHERE, podemos combinar condições no HAVING usando operadores lógicos AND, OR e NOT.
SELECT categoria,
COUNT(*) AS qtd_produtos,
AVG(preco) AS preco_medio
FROM produtos
GROUP BY categoria
HAVING COUNT(*) > 10
AND AVG(preco) < 50;
Outro exemplo com OR:
SELECT vendedor_id,
SUM(vendas) AS total_vendas,
COUNT(*) AS qtd_transacoes
FROM vendas
GROUP BY vendedor_id
HAVING SUM(vendas) > 100000
OR COUNT(*) > 50;
5. Diferenças Práticas entre WHERE e HAVING
Vamos comparar duas abordagens para o mesmo problema: encontrar departamentos com salário médio acima de 5000, considerando apenas funcionários ativos.
Abordagem 1: WHERE antes da agregação (correta)
SELECT departamento, AVG(salario) AS salario_medio
FROM funcionarios
WHERE status = 'Ativo'
GROUP BY departamento
HAVING AVG(salario) > 5000;
Abordagem 2: tentativa errada com WHERE após GROUP BY (não funciona)
-- Isso geraria erro de sintaxe
SELECT departamento, AVG(salario) AS salario_medio
FROM funcionarios
GROUP BY departamento
WHERE AVG(salario) > 5000; -- WHERE não aceita funções agregadas
Abordagem 3: filtrar no HAVING sem WHERE (menos eficiente)
SELECT departamento, AVG(salario) AS salario_medio
FROM funcionarios
GROUP BY departamento
HAVING AVG(salario) > 5000
AND status = 'Ativo'; -- Isso não funciona! status não está no GROUP BY
A regra de ouro é: use WHERE para filtrar linhas individuais antes do agrupamento e HAVING para filtrar grupos após a agregação.
6. HAVING com Subqueries
É possível usar subqueries dentro do HAVING para comparações mais sofisticadas.
SELECT produto_id, SUM(quantidade) AS total_vendido
FROM itens_venda
GROUP BY produto_id
HAVING SUM(quantidade) > (
SELECT AVG(total_por_produto)
FROM (
SELECT SUM(quantidade) AS total_por_produto
FROM itens_venda
GROUP BY produto_id
) AS medias
);
Neste exemplo, retornamos apenas os produtos cuja quantidade vendida é superior à média de todos os produtos.
Subqueries correlacionadas no HAVING também são possíveis, mas exigem cuidado com desempenho:
SELECT d.departamento, AVG(f.salario) AS media_salarial
FROM funcionarios f
JOIN departamentos d ON f.departamento_id = d.id
GROUP BY d.departamento
HAVING AVG(f.salario) > (
SELECT AVG(salario)
FROM funcionarios
WHERE departamento_id = f.departamento_id
);
7. Erros Comuns e Boas Práticas
Erro 1: Usar HAVING sem GROUP BY com colunas não agregadas
-- Erro: nome não está no GROUP BY nem é agregado
SELECT nome, COUNT(*)
FROM alunos
HAVING COUNT(*) > 1;
Erro 2: Confundir WHERE com HAVING
-- Errado: SUM não pode estar no WHERE
SELECT departamento, SUM(salario)
FROM funcionarios
WHERE SUM(salario) > 50000
GROUP BY departamento;
Boas práticas
- Sempre prefira WHERE para filtros individuais – reduz o volume de dados antes do agrupamento, melhorando performance.
- Use HAVING exclusivamente para condições pós-agregação – evite colocar filtros que poderiam estar no WHERE.
- Inclua no SELECT apenas colunas do GROUP BY ou funções de agregação – isso evita ambiguidades.
- Teste subqueries no HAVING com conjuntos pequenos – subqueries correlacionadas podem ser lentas em grandes volumes.
8. Exemplos Práticos e Casos de Uso
Relatório de vendas: produtos com faturamento acima da média
SELECT produto_id,
SUM(quantidade * preco_unitario) AS faturamento
FROM itens_venda
GROUP BY produto_id
HAVING SUM(quantidade * preco_unitario) > (
SELECT AVG(faturamento_produto)
FROM (
SELECT SUM(quantidade * preco_unitario) AS faturamento_produto
FROM itens_venda
GROUP BY produto_id
) AS sub
);
Análise de alunos: turmas com média superior a 8 e mínimo de 20 alunos
SELECT turma,
AVG(nota_final) AS media_turma,
COUNT(*) AS total_alunos
FROM matriculas m
JOIN notas n ON m.aluno_id = n.aluno_id
WHERE m.ano_letivo = 2024
GROUP BY turma
HAVING AVG(n.nota_final) > 8
AND COUNT(*) >= 20;
Dashboard de RH: departamentos com mais de 5 funcionários e salário médio > 5000
SELECT d.nome_departamento,
COUNT(f.id) AS total_funcionarios,
AVG(f.salario) AS salario_medio
FROM funcionarios f
JOIN departamentos d ON f.departamento_id = d.id
WHERE f.status = 'Ativo'
GROUP BY d.nome_departamento
HAVING COUNT(f.id) > 5
AND AVG(f.salario) > 5000
ORDER BY salario_medio DESC;
Identificar clientes com pedidos acima de R$ 1000 nos últimos 30 dias
SELECT cliente_id,
COUNT(*) AS qtd_pedidos,
SUM(valor_total) AS total_gasto
FROM pedidos
WHERE data_pedido >= CURRENT_DATE - INTERVAL '30 days'
GROUP BY cliente_id
HAVING SUM(valor_total) > 1000
ORDER BY total_gasto DESC;
Referências
- Documentação PostgreSQL: HAVING Clause — Documentação oficial do PostgreSQL explicando sintaxe e comportamento da cláusula HAVING.
- W3Schools SQL HAVING — Tutorial interativo com exemplos práticos de HAVING em diferentes bancos de dados.
- SQLShack: Understanding HAVING Clause in SQL — Artigo técnico detalhado sobre diferenças entre WHERE e HAVING com exemplos reais.
- Mode Analytics: SQL HAVING Tutorial — Tutorial avançado com casos de uso em análise de dados e business intelligence.
- Oracle Documentation: HAVING Clause — Documentação oficial Oracle sobre a cláusula HAVING e sua interação com GROUP BY.
- SQL Server Docs: HAVING (Transact-SQL) — Documentação Microsoft SQL Server com exemplos de sintaxe e restrições do HAVING.
- GeeksforGeeks: SQL HAVING Clause — Explicação didática com múltiplos exemplos e exercícios práticos.