SQL para análise de dados · Guias · 30 de outubro de 2026 · 11 min de leitura

GROUP BY e HAVING no SQL: diferença entre WHERE e HAVING, com exemplos

Malas em esteiras de bagagem separadas por um funil em grupos diferentes

GROUP BY e HAVING não são alternativas: eles fazem dois trabalhos diferentes na mesma consulta. O GROUP BY junta linhas em grupos (uma linha de saída por aeroporto, por dia, por classe) para que funções de agregação como COUNT e AVG sejam calculadas por grupo. O HAVING filtra esses grupos depois que as agregações já existem. O WHERE é a terceira peça: ele filtra linhas individuais antes de qualquer agrupamento. Então a regra curta da diferença entre WHERE e HAVING é: uma condição sobre uma coluna bruta vai no WHERE; uma condição sobre uma agregação (COUNT(*) > 1000, AVG(amount) > 300) vai no HAVING.

Todos os exemplos abaixo rodam no banco de dados de uma companhia aérea usado no curso de SQL da Nomto: 33.121 voos entre 104 aeroportos entre julho e setembro de 2026, com reservas, bilhetes e trechos de voo. Os valores estão em dólares americanos e os textos dos dados em inglês. Você pode colar cada consulta na sala de SQL da Nomto e conferir a saída.

O que o GROUP BY faz

Sem GROUP BY, uma agregação transforma a tabela inteira numa linha só. Com ele, você tem uma linha por valor distinto das colunas de agrupamento.

SELECT status, COUNT(*) AS flights
FROM flights
GROUP BY status
ORDER BY flights DESC;
status flights
Arrived 16707
Scheduled 15383
On Time 518
Cancelled 414
Departed 58
Delayed 41

Seis status, seis linhas. Cada linha é um grupo, e o COUNT(*) foi calculado separadamente dentro de cada grupo.

Dá para agrupar por várias colunas e por expressões. Aqui está a contagem de assentos por modelo de avião, com uma contagem condicional dos assentos executivos na mesma passada:

SELECT aircraft_code,
       COUNT(*) AS seats,
       SUM(CASE WHEN fare_conditions = 'Business' THEN 1 ELSE 0 END) AS business
FROM seats
GROUP BY aircraft_code
ORDER BY seats DESC;
aircraft_code seats business
773 402 30
763 222 30
321 170 28
320 140 20
733 130 12
319 116 20
SU9 97 12
CR2 50 0
CN1 12 0

O que o HAVING faz

O HAVING mantém ou descarta grupos inteiros. Ele é avaliado depois que as agregações existem, então pode se referir a elas.

SELECT departure_airport, COUNT(*) AS departures
FROM flights
WHERE status = 'Arrived'
GROUP BY departure_airport
HAVING COUNT(*) > 1000
ORDER BY departures DESC;
departure_airport departures
DME 1618
SVO 1503

Leia na ordem em que o banco executa: pegue todos os voos, fique só com os que chegaram (WHERE), agrupe por aeroporto de partida, conte cada grupo, fique só com os grupos com mais de 1.000 linhas (HAVING). Dois aeroportos de Moscou, Domodedovo e Sheremetyevo, passam.

O HAVING pode usar qualquer agregação, não só a que você seleciona. Quais modelos de avião não têm classe executiva?

SELECT aircraft_code, COUNT(*) AS seats
FROM seats
GROUP BY aircraft_code
HAVING SUM(CASE WHEN fare_conditions = 'Business' THEN 1 ELSE 0 END) = 0
ORDER BY seats DESC;
aircraft_code seats
CR2 50
CN1 12

O Bombardier CRJ-200 e o Cessna 208 Caravan.

Diferença entre WHERE e HAVING

WHERE HAVING
Filtra linhas individuais grupos
Roda antes do GROUP BY depois do GROUP BY e da agregação
Aceita agregações não sim
Aceita colunas brutas qualquer coluna só colunas agrupadas (ou dentro de agregações)
Funciona sem GROUP BY sim sim, o resultado inteiro vira um grupo

A consequência prática: uma condição no WHERE muda o que entra nas agregações; uma condição no HAVING não muda. Compare estas duas consultas sobre preços de trechos.

Preço médio por classe, ficando só com os trechos que custam mais de US$ 300:

SELECT fare_conditions, COUNT(*) AS segments, ROUND(AVG(amount), 2) AS avg_price
FROM ticket_flights
WHERE amount > 300
GROUP BY fare_conditions
ORDER BY avg_price DESC;
fare_conditions segments avg_price
Business 63467 739.73
Economy 90837 500.99
Comfort 6919 474.92

Preço médio por classe, ficando só com as classes cuja média passa de US$ 300:

SELECT fare_conditions, COUNT(*) AS segments, ROUND(AVG(amount), 2) AS avg_price
FROM ticket_flights
GROUP BY fare_conditions
HAVING AVG(amount) > 300
ORDER BY avg_price DESC;
fare_conditions segments avg_price
Business 107642 511.43
Comfort 17291 327.41

A primeira consulta responde “quanto custa em média um trecho caro em cada classe”; todas as médias ficam infladas porque os trechos baratos foram descartados, e a Economy chega a passar a Comfort. A segunda responde “quais classes são caras em média”; as médias são as verdadeiras (a da Economy é US$ 159,60, então ela sai do resultado). Mesmas colunas, mesmo número, perguntas completamente diferentes.

Quando os dois funcionam, use WHERE

Uma condição sobre uma coluna agrupada pode, tecnicamente, ir em qualquer um dos dois lugares:

SELECT departure_airport, COUNT(*) AS departures
FROM flights
WHERE departure_airport IN ('SVO', 'DME', 'VKO')
GROUP BY departure_airport
ORDER BY departures DESC;
departure_airport departures
DME 3217
SVO 2981
VKO 1719

Passar a condição para HAVING departure_airport IN ('SVO', 'DME', 'VKO') devolve as mesmas três linhas. Mas a versão com HAVING primeiro agrupa e conta os 104 aeroportos e depois joga fora 101 grupos. Muitos otimizadores empurram essa condição para antes por você, mas não conte com isso; além disso, o WHERE deixa clara a sua intenção para quem lê: essas linhas estão fora do escopo desde o início.

A ordem em que o SQL executa uma consulta

Você escreve o SELECT primeiro, mas o banco avalia as cláusulas nesta ordem lógica:

  1. FROM e JOIN: montam o conjunto de linhas
  2. WHERE: descarta linhas
  3. GROUP BY: forma os grupos
  4. as agregações são calculadas por grupo
  5. HAVING: descarta grupos
  6. SELECT: calcula as colunas de saída e os aliases
  7. DISTINCT
  8. ORDER BY
  9. LIMIT / OFFSET

Quase todo erro de GROUP BY / HAVING sai desta lista:

Erros comuns

1. Uma agregação no WHERE

SELECT departure_airport, COUNT(*)
FROM flights
WHERE COUNT(*) > 1000
GROUP BY departure_airport;

Isso falha. O PostgreSQL avisa que funções de agregação não são permitidas no WHERE (“aggregate functions are not allowed in WHERE”). Leve a condição para o HAVING.

2. Uma coluna selecionada que não está agrupada nem agregada

SELECT departure_airport, arrival_airport, COUNT(*)
FROM flights
GROUP BY departure_airport;

Isso também falha: a coluna arrival_airport precisa aparecer no GROUP BY ou estar dentro de uma função de agregação. Um aeroporto de partida tem muitos aeroportos de chegada, então o banco não consegue escolher um valor por grupo. Ou acrescente arrival_airport ao GROUP BY (uma linha por rota), ou agregue a coluna, por exemplo com COUNT(DISTINCT arrival_airport).

O PostgreSQL abre uma exceção: se você agrupa pela chave primária de uma tabela, pode selecionar outras colunas dessa tabela. Agrupar por b.book_ref permite selecionar b.total_amount. Listar as duas no GROUP BY funciona em qualquer banco, por isso as consultas daqui fazem assim:

SELECT b.book_ref, COUNT(*) AS tickets, b.total_amount
FROM bookings b
JOIN tickets t ON t.book_ref = b.book_ref
GROUP BY b.book_ref, b.total_amount
HAVING COUNT(*) >= 5
ORDER BY b.total_amount DESC
LIMIT 5;
book_ref tickets total_amount
65A6EA 5 10656.00
7734A5 5 8291.00
148C46 5 8135.00
070682 5 8012.00
8DA112 5 6150.00

Só 23 reservas no banco inteiro têm cinco bilhetes ou mais.

3. Comparar grupos pequenos com grupos grandes

Atraso médio de partida por aeroporto, entre os voos que realmente decolaram:

SELECT departure_airport,
       ROUND(AVG(EXTRACT(EPOCH FROM actual_departure - scheduled_departure) / 60), 1) AS avg_delay_min,
       COUNT(*) AS departed
FROM flights
WHERE status IN ('Arrived', 'Departed')
GROUP BY departure_airport
ORDER BY avg_delay_min DESC
LIMIT 3;
departure_airport avg_delay_min departed
ULY 24.4 97
BQS 22.8 31
LPK 22.7 22

Os “piores” aeroportos têm de 22 a 97 partidas, então um ou dois atrasos longos dominam as médias. Um tamanho mínimo de grupo no HAVING deixa a comparação mais justa:

SELECT departure_airport,
       COUNT(*) AS departed,
       ROUND(AVG(EXTRACT(EPOCH FROM actual_departure - scheduled_departure) / 60), 1) AS avg_delay_min
FROM flights
WHERE status IN ('Arrived', 'Departed')
GROUP BY departure_airport
HAVING COUNT(*) >= 300
ORDER BY avg_delay_min DESC
LIMIT 5;
departure_airport departed avg_delay_min
SVX 348 14.7
VKO 870 13.4
BZK 306 13.2
KJA 358 13.1
LED 960 12.6

Repare na divisão de tarefas: o WHERE decide quais voos contam como decolados, o HAVING decide quais aeroportos têm voos suficientes.

4. COUNT(*) onde você queria COUNT(coluna)

COUNT(*) conta linhas; COUNT(coluna) conta valores não nulos. Numa consulta agrupada, a diferença muitas vezes é justamente o ponto:

SELECT status, COUNT(*) AS flights, COUNT(actual_departure) AS with_departure
FROM flights
GROUP BY status
ORDER BY flights DESC;
status flights with_departure
Arrived 16707 16707
Scheduled 15383 0
On Time 518 0
Cancelled 414 8
Departed 58 58
Delayed 41 0

Oito voos cancelados têm horário de partida registrado, uma peculiaridade dos dados que você nunca notaria só com COUNT(*). Um HAVING COUNT(actual_departure) > 0 nessa consulta manteria três grupos, não seis.

5. Agrupar por uma expressão num lugar e não no outro

Se você agrupa por data, agrupe pela mesma expressão que seleciona:

SELECT CAST(scheduled_departure AS date) AS day, COUNT(*) AS cancelled
FROM flights
WHERE status = 'Cancelled'
GROUP BY CAST(scheduled_departure AS date)
HAVING COUNT(*) >= 15
ORDER BY day;
day cancelled
2026-09-07 18
2026-09-08 21
2026-09-09 51
2026-09-10 287

Agrupar pelo timestamp bruto scheduled_departure daria um grupo por minuto de partida, e o HAVING não encontraria nada. (O acúmulo no fim da programação merece uma segunda olhada antes que alguém o reporte como crise: 403 dos 414 cancelamentos estão na programação futura de setembro.)

HAVING sem GROUP BY

Se uma consulta tem agregações e HAVING, mas não tem GROUP BY, o resultado inteiro é tratado como um grupo só. A consulta devolve uma linha ou nenhuma:

SELECT COUNT(*) AS flights
FROM flights
HAVING COUNT(*) > 30000;
flights
33121

Troque o limite para 40.000 e você recebe zero linhas. Isso às vezes é útil em verificações de dados: uma consulta que só devolve uma linha quando algo está errado.

Checklist rápido

Se houver JOIN no meio, verifique a multiplicação de linhas antes de confiar em qualquer SUM ou COUNT; o guia de JOIN no SQL trata disso. Quando você precisa de uma agregação ao lado de cada linha em vez de juntar as linhas, o que você quer são funções de janela, não GROUP BY.

Perguntas frequentes

Posso usar HAVING sem GROUP BY?

Sim. O resultado inteiro vira um grupo, e a consulta devolve uma linha ou nada, dependendo da condição do HAVING.

HAVING é mais lento que WHERE?

Para condições sobre agregações não há escolha, então a velocidade nem entra em questão. Para condições sobre colunas brutas, o WHERE deixa o banco descartar linhas antes de agrupar, o que nunca é mais lento e muitas vezes é mais rápido. Use WHERE sempre que a condição não precisar de agregação.

Posso usar um alias de coluna no HAVING?

Não no PostgreSQL nem no SQL padrão: o HAVING é avaliado antes do SELECT, então o alias ainda não existe. Repita a expressão. O MySQL e alguns outros bancos aceitam o alias como extensão.

Dá para usar WHERE e HAVING na mesma consulta?

Sim, e é comum: o WHERE escolhe as linhas que alimentam as agregações, o HAVING escolhe os grupos que ficam. Os exemplos de partidas e de atrasos acima usam os dois.

Qual a diferença entre GROUP BY e DISTINCT?

SELECT DISTINCT status FROM flights e SELECT status FROM flights GROUP BY status devolvem os mesmos seis valores. O GROUP BY também permite calcular agregações por grupo e filtrar grupos com HAVING; o DISTINCT só remove linhas duplicadas.

Outros idiomas: Español, Tiếng Việt, English, 简体中文