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

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:
FROMeJOIN: montam o conjunto de linhasWHERE: descarta linhasGROUP BY: forma os grupos- as agregações são calculadas por grupo
HAVING: descarta gruposSELECT: calcula as colunas de saída e os aliasesDISTINCTORDER BYLIMIT/OFFSET
Quase todo erro de GROUP BY / HAVING sai desta lista:
- O
WHEREroda antes de as agregações existirem, entãoWHERE COUNT(*) > 1000é impossível. - O
SELECTroda depois doGROUP BY, então cada coluna selecionada precisa ter um único valor por grupo. - O
ORDER BYroda depois doSELECT, então pode usar um alias de coluna comodepartures. - No PostgreSQL, o
HAVINGroda antes doSELECT, então não pode usar um alias doSELECT: escrevaHAVING COUNT(*) > 1000, e nãoHAVING departures > 1000. Alguns bancos (MySQL, DuckDB) aceitam o alias, e é por isso que código copiado de um banco para outro às vezes quebra aqui.
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
- A condição usa
COUNT,SUM,AVG,MIN,MAXou outra agregação? Coloque noHAVING. - É sobre os valores de uma única linha (status, data, preço de um trecho)? Coloque no
WHERE. - É só sobre uma coluna agrupada? Funciona nos dois; prefira o
WHERE. - Toda coluna do
SELECTaparece noGROUP BYou dentro de uma agregação? Se não, a consulta está errada. - No PostgreSQL, você repetiu a agregação no
HAVINGem vez de usar o alias?
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, 简体中文