Fundamentos de Programação/03 - Sistemas Reais/Semana 09 - Banco de Dados e SQL5 min
Agregações
- Por que
WHEREfiltra antes eHAVINGdepois doGROUP BY? COUNT(*)vsCOUNT(coluna): por que dão números diferentes?- O que acontece com
NULLemSUM,AVGeCOUNT? - Quando uma window function resolve o que
GROUP BYnã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:
WHEREfiltra antes de agrupar;HAVINGfiltra depois. LogoWHEREnão pode usar agregação (ela ainda não existe) eHAVINGpode.- Alias do
SELECTnão funciona noWHERE— oSELECTroda depois. Funciona noORDER BY, que roda depois doSELECT. - Toda coluna do
SELECTprecisa estar noGROUP BYou 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 |
AVG que menteAVG 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 linhascount(uf) = 3— acarlatemufNULL, eCOUNT(coluna)ignoracount(DISTINCT uf) = 2— sóSPeRJ(oSPaparece 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: WHERE → GROUP BY → HAVING. 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.
COALESCEcompra 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(...).Scancom 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. Índices —
GROUP BYsobre 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