🧠FFVAcademy
📊

GROUP BY, HAVING e agregações que resolvem 80% dos casos

11 min de leitura·+55 XP

Agregações são o que transformam linhas individuais em métricas de negócio. COUNT, SUM, AVG com GROUP BY resolvem a maioria dos relatórios — e FILTER, ROLLUP e DISTINCT ON resolvem os casos que parecem precisar de subqueries complicadas.

Funções de agregação essenciais

-- Funções de agregação básicas
SELECT
    COUNT(*)                   AS total_pedidos,       -- conta linhas (inclui NULL)
    COUNT(DISTINCT cliente_id) AS clientes_unicos,     -- conta valores distintos
    SUM(total)                 AS receita_total,
    AVG(total)                 AS ticket_medio,
    MIN(total)                 AS menor_pedido,
    MAX(total)                 AS maior_pedido,
    PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY total) AS mediana,
    STDDEV(total)              AS desvio_padrao
FROM pedidos
WHERE status = 'concluido';

-- GROUP BY: agrupa linhas e aplica agregação por grupo
SELECT
    DATE_TRUNC('month', criado_em) AS mes,
    status,
    COUNT(*) AS num_pedidos,
    SUM(total) AS receita
FROM pedidos
GROUP BY DATE_TRUNC('month', criado_em), status
ORDER BY mes DESC, status;

-- Regra do GROUP BY: SELECT pode ter apenas
-- ✅ Colunas do GROUP BY
-- ✅ Funções de agregação
-- ❌ Outras colunas (erro: "must appear in GROUP BY clause")

-- HAVING: filtra após agregação
SELECT
    cliente_id,
    COUNT(*) AS num_pedidos,
    SUM(total) AS total_gasto
FROM pedidos
WHERE criado_em >= '2024-01-01'         -- filtra ANTES de agrupar
GROUP BY cliente_id
HAVING SUM(total) > 1000                -- filtra DEPOIS de agrupar
ORDER BY total_gasto DESC
LIMIT 20;

FILTER: agregações condicionais numa passagem

-- Relatório de pedidos por status — SEM FILTER (3 subqueries ou pivô manual)
SELECT
    COUNT(*) FILTER (WHERE status = 'pendente')   AS pendentes,
    COUNT(*) FILTER (WHERE status = 'processando') AS processando,
    COUNT(*) FILTER (WHERE status = 'concluido')  AS concluidos,
    COUNT(*) FILTER (WHERE status = 'cancelado')  AS cancelados,
    SUM(total) FILTER (WHERE status = 'concluido') AS receita_realizada,
    SUM(total) FILTER (WHERE status = 'pendente')  AS receita_potencial
FROM pedidos
WHERE criado_em >= NOW() - INTERVAL '30 days';

-- Alternativa menos legível (CASE WHEN dentro de agregação):
SELECT
    COUNT(CASE WHEN status = 'pendente' THEN 1 END) AS pendentes,
    SUM(CASE WHEN status = 'concluido' THEN total ELSE 0 END) AS receita
FROM pedidos;

-- Análise de cohort: primeiros 30 dias vs depois
SELECT
    DATE_TRUNC('month', primeiro_pedido.data) AS cohort,
    COUNT(DISTINCT p.cliente_id) AS total_clientes,
    COUNT(DISTINCT p.cliente_id) FILTER (
        WHERE p.criado_em <= primeiro_pedido.data + INTERVAL '30 days'
    ) AS compraram_no_primeiro_mes
FROM pedidos p
JOIN (
    SELECT cliente_id, MIN(criado_em) AS data FROM pedidos GROUP BY cliente_id
) primeiro_pedido USING (cliente_id)
GROUP BY DATE_TRUNC('month', primeiro_pedido.data)
ORDER BY cohort;

ROLLUP, CUBE e GROUPING SETS

-- ROLLUP: subtotais hierárquicos
SELECT
    COALESCE(DATE_TRUNC('year', criado_em)::TEXT, 'TOTAL') AS ano,
    COALESCE(DATE_TRUNC('month', criado_em)::TEXT, 'SUBTOTAL') AS mes,
    COUNT(*) AS pedidos,
    SUM(total) AS receita
FROM pedidos
GROUP BY ROLLUP(
    DATE_TRUNC('year', criado_em),
    DATE_TRUNC('month', criado_em)
)
ORDER BY ano NULLS LAST, mes NULLS LAST;

-- Resultado:
-- 2024-01-01 | 2024-01-01 | 150 | 45000   ← janeiro 2024
-- 2024-01-01 | 2024-02-01 | 130 | 39000   ← fevereiro 2024
-- 2024-01-01 | NULL       | 280 | 84000   ← subtotal 2024 (ROLLUP!)
-- NULL       | NULL       | 280 | 84000   ← grande total (ROLLUP!)

-- GROUPING SETS: especifica exatamente os agrupamentos que quer
SELECT
    produto,
    regiao,
    SUM(vendas) AS total
FROM fatos_vendas
GROUP BY GROUPING SETS (
    (produto, regiao),   -- por produto + região
    (produto),           -- só por produto
    (regiao),            -- só por região
    ()                   -- grande total
);

-- DISTINCT ON: primeiro registro de cada grupo (PostgreSQL específico)
SELECT DISTINCT ON (cliente_id)
    cliente_id,
    criado_em,
    total
FROM pedidos
ORDER BY cliente_id, criado_em DESC;   -- pega o pedido mais recente por cliente

-- Agregações de texto (string_agg é muito útil)
SELECT
    pedido_id,
    STRING_AGG(produto, ', ' ORDER BY produto) AS produtos_lista
FROM itens_pedido
GROUP BY pedido_id;
Padrões essenciais: use FILTER (WHERE ...) para múltiplas agregações condicionais em vez de várias subqueries. Use DISTINCT ON (coluna) para o primeiro/último de cada grupo em vez de window function + subquery. Use ROLLUP para relatórios com subtotais automáticos. Sempre filtre com WHERE antes de agregar — é mais eficiente que HAVING.
💡
Próximo: Window functions — ROW_NUMBER, RANK, LAG, LEAD, e SUM OVER para cálculos que precisam ver outras linhas sem GROUP BY.
🧩

Quiz rápido

3 perguntas · Acerte tudo e ganhe o badge 🎯 Gabarito

Continue lendo