QA Learning / PostgreSQL

Módulo 5: Agregação e GROUP BY

COUNT, SUM, AVG, GROUP BY, HAVING e o FILTER (WHERE …), jeito Postgres

Antes de começar

GROUP BY e HAVING funcionam igual à Trilha 1. A novidade que vale ouro é o FILTER (WHERE …): contar/somar só as linhas que batem uma condição, por coluna, sem aquele SUM(CASE WHEN … THEN 1 ELSE 0 END).

  • COUNT(*) FILTER (WHERE resultado = 'passou'): quantas passaram.
  • Dá pra ter várias na mesma query, cada uma com seu filtro, lado a lado.
  • É mais curto e mais legível que SUM(CASE …), e faz a mesma coisa.
  • bool_or(cond) / bool_and(cond): "existe alguma linha no grupo onde…" / "todas as linhas do grupo…".

Cuidado de tipo (Módulo 2): AVG de inteiros ou uma divisão inteira zeram casas decimais, então force ::numeric e ROUND(x, 2).

Olhar de QA: quase toda métrica de qualidade (taxa de aprovação, backlog por severidade, faturamento por categoria) é um GROUP BY com FILTER.

Dois exemplos

Teoria

  • COUNT(*), COUNT(coluna) (ignora NULL), SUM, AVG, MIN, MAX.
  • GROUP BY col: uma linha por valor de col. Tudo que não é agregado tem que estar no GROUP BY.
  • HAVING: filtra depois de agrupar (é o WHERE dos agregados).
  • agg FILTER (WHERE cond): agrega só as linhas que passam em cond.
  • bool_or(cond) / bool_and(cond): "algum" / "todos" dentro do grupo.
  • ROUND(AVG(x)::numeric, 2), x * 100.0 / y: pra não perder casas.

Exemplos de aplicação

Um exemplo montado pra cada item da teoria acima. Não precisa rodar, é pra ver a cara do recurso e entender cada pedaço. Os valores são ilustrativos.

Agregações + COUNT(coluna)

SELECT
  COUNT(*)          AS execucoes,
  COUNT(defeito_id) AS com_defeito,
  ROUND(AVG(duracao_seg)::numeric, 1) AS duracao_media,
  MAX(duracao_seg)  AS mais_lenta
FROM execucoes_teste;

COUNT(*) conta linhas; COUNT(defeito_id) ignora as em que a coluna é NULL. AVG/MAX resumem o resto. O ::numeric antes do ROUND é pra média sair com casas decimais.

GROUP BY

SELECT suite,
       resultado,
       COUNT(*) AS qtd
FROM execucoes_teste
GROUP BY suite, resultado
ORDER BY suite, resultado;

Agrupa por combinação de suite + resultado: uma linha por par. Regra do Postgres: toda coluna do SELECT que não está numa função de agregação precisa estar no GROUP BY.

HAVING

SELECT modulo,
       COUNT(*) AS qtd_defeitos
FROM defeitos
GROUP BY modulo
HAVING COUNT(*) > 10;

HAVING é o filtro que roda depois de agrupar; só mantém os módulos com mais de 10 defeitos. Não dá pra usar WHERE COUNT(*) > 10: o WHERE acontece antes da contagem existir.

FILTER (WHERE ...)

SELECT
  suite,
  COUNT(*)                                     AS total,
  COUNT(*) FILTER (WHERE resultado = 'passou') AS passaram,
  COUNT(*) FILTER (WHERE resultado = 'falhou') AS falharam
FROM execucoes_teste
GROUP BY suite;

FILTER aplica uma condição só àquela agregação. É a versão limpa do SUM(CASE WHEN ... THEN 1 ELSE 0 END) do SQLite: mesma ideia (contar um subconjunto do grupo), sintaxe mais direta.

bool_or / bool_and

SELECT
  pedido_id,
  bool_or(quantidade <= 0) AS tem_item_invalido,
  bool_and(quantidade > 0) AS todos_itens_ok
FROM itens_pedido
GROUP BY pedido_id;

bool_or(cond) é true se algum item do grupo satisfaz a condição; bool_and(cond) só se todos satisfazem. Ótimo pra marcar "este pedido tem pelo menos um item furado".

Porcentagem sem perder casas

SELECT
  suite,
  ROUND(
    100.0 * COUNT(*) FILTER (WHERE resultado = 'passou') / COUNT(*),
    1
  ) AS aprovacao_pct
FROM execucoes_teste
GROUP BY suite;

Multiplicar por 100.0 (com o .0) força a conta a ser decimal; senão a divisão de inteiros truncaria pra 0. ROUND(x, 1) deixa uma casa.

Pegadinhas deste módulo

Os erros que quase todo mundo comete aqui. Ler agora poupa meia hora de confusão depois, e alguns deles são a causa de relatório errado em projeto real.

  • NOT IN com NULL na subconsulta devolve zero linhas. Se a subconsulta de WHERE id NOT IN (SELECT cliente_id FROM pedidos) contiver um único NULL, o resultado é vazio, sempre, sem erro. Prefira NOT EXISTS, que não tem esse problema, ou acrescente WHERE cliente_id IS NOT NULL na subconsulta.
  • Subconsulta escalar precisa devolver exatamente uma linha. = (SELECT ...) que devolve duas linhas dá erro; que devolve nenhuma vira NULL e a comparação falha em silêncio.
  • CTE não é otimização, é legibilidade. WITH deixa a query compreensível, mas em vários bancos não torna nada mais rápido. Use pela clareza, que já é motivo suficiente.
  • EXISTS costuma ser melhor que IN em lista grande. EXISTS para assim que acha a primeira linha; IN pode materializar a lista inteira.
  • Subconsulta correlacionada roda uma vez por linha. Funciona bem em 60 clientes e trava em 6 milhões. Em banco de produção, desconfie, e teste com volume antes de rodar.

Prática

Escreva a query e clique Rodar (ou Ctrl+Enter).

carregando banco…

Query livre

Terminou este módulo? Marque: o ✓ aparece no índice da trilha e a barra de progresso anda no começo do site. Fica salvo só no seu navegador.