Banco de Dados I — 2026.2

Aula 10
Funções de Agregação, GROUP BY e HAVING

👨‍🏫 Prof. Gustavo Pinto 🏛️ UFPA 📅 28 de maio de 2026 🕣 7h30 – 9h10

Agenda de hoje

Revisão: SELECT da Aula 9

-- Estrutura básica já conhecida
SELECT coluna1, coluna2
FROM   tabela
WHERE  condicao
ORDER BY coluna ASC | DESC
LIMIT  n;

Até agora consultamos linhas individuais. Hoje vamos consultar resumos: quantos registros existem? qual a média? qual o total?

Funções de Agregação

Funções que operam sobre um conjunto de linhas e retornam um único valor resumindo aquele conjunto.

COUNT

Conta o número de linhas (ou valores não nulos).

SUM

Soma todos os valores de uma coluna numérica.

AVG

Calcula a média aritmética de uma coluna numérica.

MIN

Retorna o menor valor de uma coluna.

MAX

Retorna o maior valor de uma coluna.

COUNT — Contando Registros

-- Conta todas as linhas da tabela
SELECT COUNT(*) FROM pedido;

-- Conta quantos pedidos têm status 'entregue'
SELECT COUNT(*) FROM pedido
WHERE status = 'entregue';

-- COUNT(coluna): conta apenas valores NÃO NULOS
SELECT COUNT(dt_devolucao) FROM emprestimo;
-- ^ retorna só os empréstimos que já foram devolvidos

-- COUNT(DISTINCT): conta valores únicos
SELECT COUNT(DISTINCT id_cliente) FROM pedido;
-- ^ quantos clientes distintos fizeram ao menos 1 pedido

Atenção: COUNT(*) conta todas as linhas, incluindo NULLs. COUNT(coluna) ignora NULLs naquela coluna.

SUM e AVG

-- Valor total de todos os pedidos
SELECT SUM(valor_total) AS receita_total
FROM pedido;

-- Ticket médio dos pedidos
SELECT AVG(valor_total) AS ticket_medio
FROM pedido;

-- Combinar: total e média de pedidos entregues
SELECT
    SUM(valor_total) AS total_entregue,
    AVG(valor_total) AS media_entregue,
    COUNT(*) AS qtd_entregue
FROM pedido
WHERE status = 'entregue';

O alias AS renomeia a coluna no resultado — boa prática para colunas calculadas.

MIN e MAX

-- Produto mais barato e mais caro
SELECT
    MIN(preco) AS menor_preco,
    MAX(preco) AS maior_preco
FROM produto;

-- Data do primeiro e último empréstimo registrado
SELECT
    MIN(dt_emprestimo) AS primeiro,
    MAX(dt_emprestimo) AS ultimo
FROM emprestimo;

Funcionam com texto e datas

MIN e MAX comparam qualquer tipo ordenável: texto (alfabético), data (cronológico), número (numérico).

NULL é ignorado

Todas as funções de agregação ignoram NULL — exceto COUNT(*), que conta linhas independentemente.

GROUP BY — Agrupando Resultados

GROUP BY divide as linhas em grupos com o mesmo valor na(s) coluna(s) indicada(s). A função de agregação é aplicada a cada grupo separadamente.

-- Quantos pedidos por status?
SELECT   status, COUNT(*) AS total
FROM     pedido
GROUP BY status;

-- Resultado:
--  status     | total
-- ------------+-------
--  entregue   |  142
--  pendente   |   38
--  cancelado  |   17

Sem GROUP BY, COUNT(*) retornaria um único número: o total de pedidos.

GROUP BY — Mais Exemplos

-- Total de vendas por categoria de produto
SELECT   categoria,
         COUNT(*) AS qtd_produtos,
         AVG(preco) AS preco_medio
FROM     produto
GROUP BY categoria
ORDER BY preco_medio DESC;

-- Faturamento total por cliente
SELECT   id_cliente,
         COUNT(*) AS num_pedidos,
         SUM(valor_total) AS faturamento
FROM     pedido
GROUP BY id_cliente
ORDER BY faturamento DESC
LIMIT    5;

Regra do GROUP BY: toda coluna no SELECT que não seja uma agregação deve aparecer no GROUP BY.

HAVING — Filtrando Grupos

HAVING filtra grupos após a agregação — o equivalente do WHERE aplicado ao resultado do GROUP BY.

-- Clientes que fizeram mais de 5 pedidos
SELECT   id_cliente, COUNT(*) AS total_pedidos
FROM     pedido
GROUP BY id_cliente
HAVING   COUNT(*) > 5;

-- Categorias com preço médio acima de 200
SELECT   categoria, AVG(preco) AS preco_medio
FROM     produto
GROUP BY categoria
HAVING   AVG(preco) > 200
ORDER BY preco_medio DESC;

WHERE vs HAVING

WHERE

Filtra linhas individuais antes do agrupamento. Não pode referenciar funções de agregação.

→ "Quero apenas pedidos de 2026."

HAVING

Filtra grupos depois da agregação. Pode (e deve) referenciar funções de agregação.

→ "Quero grupos com mais de 10 pedidos."

-- Usando WHERE e HAVING juntos
SELECT   id_cliente, COUNT(*) AS pedidos_2026
FROM     pedido
WHERE    EXTRACT(YEAR FROM dt_pedido) = 2026   -- filtra linhas antes
GROUP BY id_cliente
HAVING   COUNT(*) >= 3                         -- filtra grupos depois
ORDER BY pedidos_2026 DESC;

Ordem de Execução do SELECT

① FROM

Identifica a(s) tabela(s) de origem dos dados.

② WHERE

Filtra as linhas antes do agrupamento.

③ GROUP BY

Agrupa as linhas restantes.

④ HAVING

Filtra os grupos formados.

⑤ SELECT

Projeta as colunas e aplica as funções de agregação.

⑥ ORDER BY / LIMIT

Ordena e limita o resultado final.

Entender essa ordem explica por que WHERE não pode usar agregações — elas ainda não foram calculadas nesse momento.

Exercício Reflexivo

Pensando criticamente sobre agregação

Discuta com um colega. O objetivo é argumentar, não apenas responder.

Questão 1

Uma query retorna a média de salários por departamento. Um analista quer excluir departamentos com menos de 3 funcionários, pois a média seria "pouco representativa". Como você faria isso? Por que usar HAVING e não WHERE?

Questão 2

COUNT(*) e COUNT(coluna) podem retornar valores diferentes para a mesma tabela. Em que situação isso acontece? Isso é um bug ou comportamento esperado? Quando cada um é o correto a usar?

Laboratório Prático

Sistema de Biblioteca — Consultas Analíticas

Use o banco da Aula 7 (biblioteca com livros, membros e empréstimos) no DB Fiddle (PostgreSQL).

① Contagens

Quantos livros há por editora? Quantos membros cadastrados? Quantos empréstimos ainda estão em aberto (dt_devolucao IS NULL)?

② Agrupamentos

Quantos empréstimos cada membro já fez? Liste por nome (ORDER BY total DESC). Qual a média de dias de empréstimo por livro devolvido?

③ Filtrando grupos

Liste apenas os membros que têm mais de 2 empréstimos. Liste as editoras que publicaram mais de 1 livro no banco. Use HAVING em ambos os casos.

Próxima Aula — 02/06 (Ter)

JOINs
INNER JOIN, LEFT JOIN, RIGHT JOIN