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

  1. Sempre prefira WHERE para filtros individuais – reduz o volume de dados antes do agrupamento, melhorando performance.
  2. Use HAVING exclusivamente para condições pós-agregação – evite colocar filtros que poderiam estar no WHERE.
  3. Inclua no SELECT apenas colunas do GROUP BY ou funções de agregação – isso evita ambiguidades.
  4. 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