Pessoa analisando dados em um tablet ao lado de um computador
Agrupar dados transforma linhas individuais em respostas resumidas. Crédito da foto.

Quando uma tabela cresce, olhar registro por registro deixa de responder perguntas como “quantos produtos existem em cada categoria?”, “qual é o valor médio?” ou “quais categorias têm estoque total acima de 100?”. Em SQL, as funções de agregação resumem várias linhas, GROUP BY separa essas linhas em grupos e HAVING filtra os grupos depois do cálculo.

Comece pelas funções de agregação

As mais úteis no início são COUNT, SUM, AVG, MIN e MAX. Sem agrupamento, elas resumem o conjunto inteiro selecionado.

SELECT COUNT(*) AS total_produtos,
       AVG(preco) AS preco_medio,
       MIN(preco) AS menor_preco,
       MAX(preco) AS maior_preco
FROM produtos;

COUNT(*) conta linhas. AVG(preco) calcula a média dos valores não nulos da coluna. MIN e MAX encontram os extremos.

GROUP BY cria um resumo para cada grupo

Agora imagine que a pergunta seja “quantos produtos existem em cada categoria?”. A categoria precisa definir os grupos:

SELECT categoria, COUNT(*) AS quantidade
FROM produtos
GROUP BY categoria
ORDER BY quantidade DESC;

O banco reúne as linhas que têm a mesma categoria e calcula COUNT(*) separadamente para cada conjunto. O resultado tem uma linha por categoria, não uma linha por produto.

Somando estoque por categoria

SELECT categoria,
       SUM(estoque) AS estoque_total,
       ROUND(AVG(preco), 2) AS preco_medio
FROM produtos
GROUP BY categoria
ORDER BY categoria;

A mesma consulta pode calcular mais de um resumo por grupo. O ROUND apenas facilita a leitura da média.

Regra prática: se uma coluna aparece no SELECT sem estar dentro de uma função de agregação, normalmente ela precisa participar do GROUP BY.

WHERE filtra linhas; HAVING filtra grupos

Essa diferença é central. WHERE atua antes do agrupamento. HAVING testa o resultado produzido para cada grupo.

SELECT categoria, SUM(estoque) AS estoque_total
FROM produtos
WHERE preco > 20
GROUP BY categoria
HAVING SUM(estoque) > 100
ORDER BY estoque_total DESC;

Primeiro entram apenas produtos com preço acima de 20. Depois eles são agrupados por categoria. Por fim, permanecem somente as categorias cujo estoque somado supera 100.

Um erro que ajuda a entender

-- Evite isto:
SELECT categoria, COUNT(*)
FROM produtos
WHERE COUNT(*) > 3
GROUP BY categoria;

WHERE não pode decidir com base em COUNT(*) porque a contagem ainda não foi produzida nessa etapa. A condição agregada pertence ao HAVING:

SELECT categoria, COUNT(*) AS quantidade
FROM produtos
GROUP BY categoria
HAVING COUNT(*) > 3;

Laboratório: relatório de vendas no PostgreSQL

Crie uma tabela pequena para experimentar sem depender de outro projeto.

CREATE TABLE vendas (
  id SERIAL PRIMARY KEY,
  categoria VARCHAR(40) NOT NULL,
  produto VARCHAR(80) NOT NULL,
  quantidade INTEGER NOT NULL,
  valor_unitario NUMERIC(10,2) NOT NULL
);
INSERT INTO vendas (categoria, produto, quantidade, valor_unitario) VALUES
('Informática','Mouse',3,90.00),
('Informática','Teclado',2,150.00),
('Informática','Monitor',1,900.00),
('Papelaria','Caderno',10,22.00),
('Papelaria','Caneta',30,4.50),
('Acessórios','Suporte',4,75.00);

1. Faturamento por categoria

SELECT categoria,
       SUM(quantidade * valor_unitario) AS faturamento
FROM vendas
GROUP BY categoria
ORDER BY faturamento DESC;

A expressão multiplica quantidade por valor unitário em cada linha; depois SUM soma esses valores dentro de cada categoria.

2. Categorias que venderam pelo menos cinco unidades

SELECT categoria, SUM(quantidade) AS unidades
FROM vendas
GROUP BY categoria
HAVING SUM(quantidade) >= 5
ORDER BY unidades DESC;

3. Média de preço somente para itens com valor acima de R$ 20

SELECT categoria,
       ROUND(AVG(valor_unitario), 2) AS media
FROM vendas
WHERE valor_unitario > 20
GROUP BY categoria
ORDER BY media DESC;

Observe que o filtro individual usa WHERE. Se a exigência fosse “mostrar somente categorias cuja média seja maior que 100”, seria HAVING AVG(valor_unitario) > 100.

Desafio prático

Produza um relatório com categoria, quantidade total de unidades e faturamento total. Mostre somente categorias com faturamento acima de R$ 300 e ordene do maior faturamento para o menor. Antes de executar, escreva em uma frase o papel de GROUP BY, HAVING e ORDER BY na sua consulta.

Quatro erros comuns

1. Coluna solta no SELECT. Misturar uma coluna comum com agregação sem agrupá-la costuma gerar erro. 2. Usar WHERE para filtrar uma soma ou média. Condições sobre resultados agregados pertencem ao HAVING. 3. Confundir COUNT(*) com COUNT(coluna). O primeiro conta linhas; o segundo ignora valores nulos naquela coluna. 4. Agrupar demais. Cada coluna adicional no GROUP BY divide os dados em grupos mais específicos e pode destruir o resumo desejado.

Caso completo: relatório mensal que pode ser conferido

Uma consulta deve mostrar categorias com pelo menos três vendas concluídas e faturamento acima de R$ 500. Pedidos cancelados não entram. O filtro de status atua nas linhas antes do agrupamento; as regras sobre quantidade e soma atuam depois.

SELECT categoria,
       COUNT(*) AS quantidade_vendas,
       SUM(valor_total) AS faturamento
FROM vendas
WHERE status = 'CONCLUIDA'
GROUP BY categoria
HAVING COUNT(*) >= 3
   AND SUM(valor_total) > 500
ORDER BY faturamento DESC;

Como conferir sem confiar cegamente na consulta

  1. Filtre manualmente uma amostra e retire os cancelados.
  2. Separe as linhas por categoria.
  3. Conte vendas e some valores de cada grupo.
  4. Aplique os dois critérios do HAVING.
  5. Compare colunas, ordem e quantidade de grupos com o SQL.
Etapa lógicaPapelErro comum
WHEREEscolher linhas válidasFiltrar agregados que ainda não existem
GROUP BYFormar gruposSelecionar coluna sem agrupar nem agregar
HAVINGEscolher gruposUsá-lo no lugar de todo filtro de linha
ORDER BYApresentar o resultadoSupor uma ordem sem declará-la

Teste de consistência

Crie duas versões do relatório: uma detalhada, com as linhas que participam, e outra agrupada. Use a primeira para justificar cada total da segunda. Inclua uma categoria com duas vendas, outra com três que somam exatamente R$ 500 e outra que atende aos dois critérios.

Perguntas frequentes

Posso usar GROUP BY sem função de agregação?

Sim, mas muitas vezes DISTINCT comunica melhor a intenção quando você só quer combinações únicas. O maior valor do GROUP BY aparece quando ele organiza cálculos por grupo.

HAVING substitui WHERE?

Não. Use WHERE para excluir linhas antes da agregação e HAVING para excluir grupos depois dela.

COUNT(*) conta valores NULL?

Ele conta as linhas. Já COUNT(nome_da_coluna) conta somente as linhas em que essa coluna não é nula.

Conclusão

O salto de consultas simples para relatórios começa quando você deixa de perguntar apenas “quais linhas existem?” e passa a perguntar “o que esses dados mostram em conjunto?”. Funções de agregação fazem os cálculos, GROUP BY define os grupos e HAVING seleciona os grupos relevantes. Com isso, SQL passa a responder perguntas de análise sem precisar exportar os dados para outra ferramenta.

Referências bibliográficas

  1. BEAULIEU, Alan. Learning SQL . 3. ed. O’Reilly Media, 2020.
  2. ELMASRI, Ramez; NAVATHE, Shamkant B. Fundamentals of Database Systems . 7. ed. Pearson, 2022.

Documentação técnica: PostgreSQL Global Development Group. Aggregate Functions e tutorial de agregação.