trilha

Fundamentos de Programação/03 - Sistemas Reais/Semana 09 - Banco de Dados e SQL5 min

Agregações

Perguntas-guia
  • Por que WHERE filtra antes e HAVING depois do GROUP BY?
  • COUNT(*) vs COUNT(coluna): por que dão números diferentes?
  • O que acontece com NULL em SUM, AVG e COUNT?
  • Quando uma window function resolve o que GROUP BY não resolve?

Conceito

GROUP BY particiona as linhas em grupos; a função de agregação colapsa cada grupo numa linha. Tudo o que confunde em agregação vem da ordem lógica de avaliação:

FROM / JOIN  ->  WHERE  ->  GROUP BY  ->  HAVING  ->  SELECT  ->  ORDER BY  ->  LIMIT

Dessa ordem decorrem três fatos que parecem arbitrários e não são:

  1. WHERE filtra antes de agrupar; HAVING filtra depois. Logo WHERE não pode usar agregação (ela ainda não existe) e HAVING pode.
  2. Alias do SELECT não funciona no WHERE — o SELECT roda depois. Funciona no ORDER BY, que roda depois do SELECT.
  3. Toda coluna do SELECT precisa estar no GROUP BY ou dentro de uma agregação — o grupo colapsou, e uma coluna não agregada não tem valor único.

NULL em agregação é a segunda fonte de erro, e a regra é uniforme:

Função Trata NULL como
COUNT(*) conta linhas, NULL incluído
COUNT(coluna) ignora NULLs
COUNT(DISTINCT coluna) ignora NULLs e duplicatas
SUM, AVG, MIN, MAX ignoram NULLs
O AVG que mente

AVG ignora NULLs no numerador e no denominador. Se metade das notas é NULL, a média é a média das preenchidas — não a média sobre todos os alunos.

Se você queria tratar NULL como zero: AVG(COALESCE(nota, 0)). As duas respostas são legítimas; o erro é não saber qual você pediu.

SUM sobre conjunto vazio devolve NULL, não 0. Um relatório que soma pedidos de um mês sem vendas devolve NULL, e NULL + qualquer coisa = NULL — o erro se propaga silenciosamente pela planilha inteira.

Window function é o que resolve o caso "preciso do agregado e do detalhe". GROUP BY colapsa; OVER() calcula sem colapsar:

SELECT nome, total, sum(total) OVER (PARTITION BY cliente_id) AS total_cliente
FROM pedido;   -- cada linha mantém o detalhe E vê o agregado do grupo

Na prática

Verificado com 4 clientes, um deles (carla) com uf NULL:

SELECT count(*) AS estrela, count(uf) AS conta_uf, count(DISTINCT uf) AS distintos
FROM cliente;
estrela  conta_uf  distintos
      4         3          2

Três números diferentes sobre a mesma tabela:

  • count(*) = 4 — quatro linhas
  • count(uf) = 3 — a carla tem uf NULL, e COUNT(coluna) ignora
  • count(DISTINCT uf) = 2 — só SP e RJ (o SP aparece duas vezes)

Se alguém pergunta "quantos clientes temos?", as três respostas são defensáveis e apenas uma está certa para a pergunta real.

Em Go

O ponto de atrito é SUM devolvendo NULL sobre conjunto vazio. Isso quebra o Scan:

var total int64
err := db.QueryRow("SELECT sum(total) FROM pedido WHERE cliente_id = 999").Scan(&total)
// sql: Scan error: converting NULL to int64 is unsupported

Três soluções, em ordem de preferência:

SELECT COALESCE(sum(total), 0) FROM pedido WHERE cliente_id = $1   -- 1. resolve no SQL
var total sql.NullInt64      // 2. trata em Go, explicitamente
var total *int64             // 3. idem, mais conciso

A primeira é a melhor porque a decisão ("mês sem vendas vale zero") é de negócio e pertence à consulta, não ao código de scan.

QueryRow e sql.ErrNoRows: agregação sempre devolve uma linha (mesmo vazia), então ErrNoRows não acontece com SUM/COUNT. Já SELECT nome FROM ... sem resultado devolve sql.ErrNoRows, e tratar isso como erro de infra é um bug comum:

if errors.Is(err, sql.ErrNoRows) { /* não encontrado: 404, não 500 */ }

Agregação em Go ou no banco? Regra prática: se você vai descartar as linhas depois de somar, some no banco — trazer 100 mil linhas para calcular uma média é transferir 100 mil linhas pela rede para jogar fora.

Respostas às perguntas-guia

1. Por que WHERE filtra antes e HAVING depois do GROUP BY?

Por definição da ordem lógica: WHEREGROUP BYHAVING. Consequência: WHERE não pode referenciar agregação; HAVING pode. E um filtro que não depende de agregação deve ir no WHERE — ali ele reduz o volume antes de agrupar, o que é mais rápido.

2. COUNT(*) vs COUNT(coluna): por que dão números diferentes?

COUNT(*) conta linhas; COUNT(coluna) ignora NULLs. Verificado: 4 contra 3.

Em Go: nenhuma diferença no Scan — os dois devolvem inteiro não-nulo. O risco é semântico, não de tipo.

3. O que acontece com NULL em SUM, AVG e COUNT?

Todos ignoram, e o AVG ignora também no denominador. SUM sobre conjunto vazio devolve NULL, não zero.

Em Go, esse NULL é o que quebra o Scan em int64. COALESCE(sum(x), 0) no SQL.

4. Quando uma window function resolve o que GROUP BY não resolve?

Quando você precisa do detalhe e do agregado na mesma linha: percentual do total, ranking, média móvel, comparação com o anterior (LAG). GROUP BY colapsa e você perde o detalhe; OVER() não colapsa.

Trade-offs

Do conceito:

  • Agregar no banco compra menos tráfego e usa o índice; cobra SQL mais complexo e menos testável unitariamente.
  • Agregar na aplicação compra testabilidade e cobra transferir tudo pela rede.
  • COALESCE compra robustez e apaga a distinção entre "zero" e "não há dado".
  • Window function compra expressividade e cobra custo (ordenação por partição) e suporte do banco.

Em Go:

  • Resolver NULL no SQL mantém o Go simples; resolver com sql.Null* mantém a informação de ausência.
  • QueryRow(...).Scan com agregação nunca dá ErrNoRows — não escreva esse tratamento achando que protege.

Comandos e consultas essenciais

-- filtro antes (WHERE) e depois (HAVING) da agregação
SELECT cliente_id, count(*) AS n, sum(total) AS soma
FROM pedido
WHERE status = 'pago'            -- reduz ANTES de agrupar: mais rápido
GROUP BY cliente_id
HAVING count(*) > 2              -- filtra o RESULTADO da agregação
ORDER BY soma DESC;

-- os três counts, que respondem perguntas diferentes
SELECT count(*), count(uf), count(DISTINCT uf) FROM cliente;

-- SUM sobre vazio: NULL. COALESCE resolve na consulta.
SELECT COALESCE(sum(total), 0) FROM pedido WHERE cliente_id = 999;

-- detalhe E agregado na mesma linha
SELECT id, total,
       sum(total)   OVER (PARTITION BY cliente_id) AS total_cliente,
       100.0*total / sum(total) OVER (PARTITION BY cliente_id) AS pct,
       row_number() OVER (PARTITION BY cliente_id ORDER BY total DESC) AS posicao
FROM pedido;

Relacionado

  • 1. Joins — fan-out do join inflaciona agregação
  • 3. Subqueries — agregação correlacionada sem fan-out
  • 5. ÍndicesGROUP BY sobre coluna indexada evita ordenação
  • 4. Mutabilidade — semana 1, NULL é a terceira lógica que quebra intuição

Parte de Semana 09 - Banco de Dados e SQL · 00 - MOC Fundamentos de Programação

Buscar

Busca por título, seção e texto das notas