SQL para análise de dados · Guias · 30 de outubro de 2026 · 14 min de leitura
CASE WHEN no SQL: condições, categorias e agregação condicional com exemplos

O CASE é o “se-então-senão” do SQL. Ele olha cada linha, verifica as condições em ordem e devolve o valor da primeira condição verdadeira. Se nenhuma for verdadeira, devolve o valor do ELSE, ou NULL se não houver ELSE:
CASE
WHEN condition_1 THEN value_1
WHEN condition_2 THEN value_2
ELSE fallback_value
END
O CASE é uma expressão, não um comando, então pode ir em qualquer lugar onde cabe um valor: no SELECT, no WHERE, no GROUP BY, no ORDER BY e dentro de funções de agregação. Esse último uso, a agregação condicional, é onde o CASE faz o trabalho mais valioso para quem analisa dados.
Os exemplos 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, assentos e um programa de fidelidade. Os valores estão em dólares americanos e os textos dos dados em inglês. O SQL segue a sintaxe do PostgreSQL, e cada resultado saiu da execução real da consulta.
CASE com condições: qualquer condição
A forma mais comum, chamada de CASE pesquisado (searched), aceita uma condição completa depois de cada WHEN. Separando a frota por categoria de alcance:
SELECT model, range,
CASE
WHEN range >= 6000 THEN 'long-haul'
WHEN range >= 3000 THEN 'medium-haul'
ELSE 'short-haul'
END AS category
FROM aircrafts
ORDER BY range DESC;
| model | range | category |
|---|---|---|
| Boeing 777-300 | 11100 | long-haul |
| Boeing 767-300 | 7900 | long-haul |
| Airbus A319-100 | 6700 | long-haul |
| Airbus A320-200 | 5700 | medium-haul |
| Airbus A321-200 | 5600 | medium-haul |
| Boeing 737-300 | 4200 | medium-haul |
| Sukhoi Superjet-100 | 3000 | medium-haul |
| Bombardier CRJ-200 | 2700 | short-haul |
| Cessna 208 Caravan | 1200 | short-haul |
Repare que o segundo ramo diz só range >= 3000, e não range >= 3000 AND range < 6000. Ele não precisa do limite superior, porque as linhas com 6.000 ou mais já foram pegas pelo primeiro ramo. Essa é a regra que faz o CASE funcionar: os ramos são verificados de cima para baixo, e a primeira correspondência vence.
CASE simples: comparar uma expressão com valores
Quando todos os ramos comparam a mesma expressão com uma constante, a forma simples é mais curta:
SELECT aircraft_code, model,
CASE aircraft_code
WHEN 'SU9' THEN 'Sukhoi'
WHEN 'CR2' THEN 'Bombardier'
WHEN 'CN1' THEN 'Cessna'
ELSE split_part(model, ' ', 1)
END AS maker
FROM aircrafts
ORDER BY aircraft_code;
| aircraft_code | model | maker |
|---|---|---|
| 319 | Airbus A319-100 | Airbus |
| 320 | Airbus A320-200 | Airbus |
| 321 | Airbus A321-200 | Airbus |
| 733 | Boeing 737-300 | Boeing |
| 763 | Boeing 767-300 | Boeing |
| 773 | Boeing 777-300 | Boeing |
| CN1 | Cessna 208 Caravan | Cessna |
| CR2 | Bombardier CRJ-200 | Bombardier |
| SU9 | Sukhoi Superjet-100 | Sukhoi |
CASE aircraft_code WHEN 'SU9' significa CASE WHEN aircraft_code = 'SU9'. Os valores também podem ser expressões: o ELSE aqui pega a primeira palavra do nome do modelo. (Neste caso, todos os ramos acabam dando a primeira palavra de qualquer jeito; em dados reais, os ramos explícitos são onde ficam as exceções.) A forma simples só testa igualdade, então para faixas, LIKE, IN ou verificações de NULL você precisa da forma com condições.
Separar números em faixas
Transformar um número contínuo em faixas é o trabalho do dia a dia do CASE. Como se distribuem os preços dos trechos?
SELECT CASE
WHEN amount < 100 THEN 'under $100'
WHEN amount < 300 THEN '$100-299'
WHEN amount < 600 THEN '$300-599'
ELSE '$600+'
END AS price_band,
COUNT(*) AS segments
FROM ticket_flights
GROUP BY price_band
ORDER BY MIN(amount);
| price_band | segments |
|---|---|
| under $100 | 359344 |
| $100-299 | 525159 |
| $300-599 | 93383 |
| $600+ | 67840 |
Dois truques pequenos aqui. Agrupar pelo alias price_band funciona no PostgreSQL e no DuckDB (o SQL padrão estrito obrigaria você a repetir o CASE inteiro ou usar a posição, GROUP BY 1). E ORDER BY MIN(amount) ordena as faixas pela ordem real, e não pela ordem alfabética, em que $100-299 viria antes de under $100.
A mesma ideia funciona com intervalos de tempo. Quanto atrasaram na partida os voos que chegaram?
SELECT CASE
WHEN actual_departure - scheduled_departure <= INTERVAL '5 minutes' THEN 'up to 5 min'
WHEN actual_departure - scheduled_departure <= INTERVAL '15 minutes' THEN '6-15 min'
WHEN actual_departure - scheduled_departure <= INTERVAL '3 hours' THEN '15 min to 3 h'
ELSE 'over 3 h'
END AS delay_group,
COUNT(*) AS flights
FROM flights
WHERE status = 'Arrived'
GROUP BY delay_group
ORDER BY MIN(actual_departure - scheduled_departure);
| delay_group | flights |
|---|---|
| up to 5 min | 14657 |
| 6-15 min | 1254 |
| 15 min to 3 h | 209 |
| over 3 h | 587 |
A primeira correspondência vence: a ordem dos ramos importa
Coloque os ramos na ordem errada e uma categoria some em silêncio:
SELECT CASE
WHEN actual_departure - scheduled_departure > INTERVAL '15 minutes' THEN 'late'
WHEN actual_departure - scheduled_departure > INTERVAL '3 hours' THEN 'very late'
ELSE 'on time'
END AS delay_group,
COUNT(*) AS flights
FROM flights
WHERE status = 'Arrived'
GROUP BY delay_group
ORDER BY flights DESC;
| delay_group | flights |
|---|---|
| on time | 15911 |
| late | 796 |
Todo voo com mais de três horas de atraso também tem mais de 15 minutos de atraso, então o primeiro ramo fica com ele e o very late nunca dispara. Nenhum erro, nenhum aviso, só uma categoria com zero linhas que ninguém percebe que está faltando. Com condições de “maior que”, coloque o maior limite primeiro; com “menor que”, o menor primeiro, como nas faixas de preço acima.
CASE e NULL
Duas regras de NULL pegam as pessoas desprevenidas no CASE.
Uma condição que dá NULL conta como falsa, então a linha cai no ELSE. No programa de fidelidade, birth_year está vazio para alguns membros. Veja o que isto faz:
SELECT CASE
WHEN birth_year < 1980 THEN 'born before 1980'
ELSE 'born 1980 or later'
END AS age_group,
COUNT(*) AS members
FROM loyalty_members
GROUP BY age_group
ORDER BY age_group;
| age_group | members |
|---|---|
| born 1980 or later | 35666 |
| born before 1980 | 17334 |
Os 6.319 membros sem ano de nascimento foram rotulados como “born 1980 or later”. Trate o NULL num ramo próprio, primeiro:
SELECT CASE
WHEN birth_year IS NULL THEN 'unknown'
WHEN birth_year < 1980 THEN 'born before 1980'
ELSE 'born 1980 or later'
END AS age_group,
COUNT(*) AS members
FROM loyalty_members
GROUP BY age_group
ORDER BY age_group;
| age_group | members |
|---|---|
| born 1980 or later | 29347 |
| born before 1980 | 17334 |
| unknown | 6319 |
O CASE simples não consegue encontrar NULL. CASE birth_year WHEN NULL THEN 'unknown' ELSE 'known' END rotula todos os 53.000 membros como “known”, porque testa birth_year = NULL, que nunca é verdadeiro. Use WHEN birth_year IS NULL na forma com condições.
E se você deixa o ELSE de fora, as linhas sem correspondência recebem NULL:
SELECT model,
CASE
WHEN range >= 6000 THEN 'long-haul'
WHEN range >= 3000 THEN 'medium-haul'
END AS category
FROM aircrafts
WHERE range < 3000;
| model | category |
|---|---|
| Cessna 208 Caravan | NULL |
| Bombardier CRJ-200 | NULL |
Às vezes é exatamente isso que você quer (é a base do truque com AVG mais abaixo). Quando não é, um ELSE 'other' explícito deixa as lacunas visíveis. Mais sobre o comportamento do NULL no guia de NULL no SQL.
CASE no ORDER BY: uma ordenação personalizada
A ordem alfabética raramente coincide com a ordem do negócio. Os status de voo têm um ciclo de vida natural:
SELECT status, COUNT(*) AS flights
FROM flights
GROUP BY status
ORDER BY CASE status
WHEN 'Scheduled' THEN 1
WHEN 'On Time' THEN 2
WHEN 'Delayed' THEN 3
WHEN 'Departed' THEN 4
WHEN 'Arrived' THEN 5
ELSE 6
END;
| status | flights |
|---|---|
| Scheduled | 15383 |
| On Time | 518 |
| Delayed | 41 |
| Departed | 58 |
| Arrived | 16707 |
| Cancelled | 414 |
O CASE devolve um número usado só para ordenar; ele não aparece na saída. O ELSE 6 manda qualquer valor inesperado, aqui Cancelled, para o fim, em vez de bagunçar a ordem.
CASE no GROUP BY: seus próprios segmentos
Você pode agrupar por um CASE para criar segmentos que não existem como coluna. Os fins de semana têm mais voos por dia que os dias úteis?
SELECT CASE
WHEN EXTRACT(ISODOW FROM scheduled_departure) IN (6, 7) THEN 'weekend'
ELSE 'weekday'
END AS day_type,
COUNT(*) AS flights,
COUNT(DISTINCT CAST(scheduled_departure AS date)) AS days,
ROUND(COUNT(*) * 1.0 / COUNT(DISTINCT CAST(scheduled_departure AS date)), 1) AS flights_per_day
FROM flights
GROUP BY day_type
ORDER BY day_type;
| day_type | flights | days | flights_per_day |
|---|---|---|---|
| weekday | 23555 | 44 | 535.3 |
| weekend | 9566 | 17 | 562.7 |
Comparar os totais brutos (23.555 contra 9.566) não faria sentido, já que há muito mais dias úteis. Dividir pelo número de dias de cada segmento dá uma comparação justa: os fins de semana são cerca de 5% mais movimentados.
Agregação condicional
Coloque um CASE dentro de uma agregação e você calcula várias métricas filtradas numa única passada pela tabela, uma por coluna. Este é o padrão de CASE mais útil em análise de dados.
Contar com SUM(CASE … THEN 1 ELSE 0 END)
Quantos voos cada aeroporto grande cancelou, e que percentual isso representa?
SELECT departure_airport,
COUNT(*) AS flights,
SUM(CASE WHEN status = 'Cancelled' THEN 1 ELSE 0 END) AS cancelled,
ROUND(100.0 * SUM(CASE WHEN status = 'Cancelled' THEN 1 ELSE 0 END) / COUNT(*), 2) AS cancel_pct
FROM flights
WHERE departure_airport IN ('SVO', 'DME', 'VKO', 'LED')
GROUP BY departure_airport
ORDER BY departure_airport;
| departure_airport | flights | cancelled | cancel_pct |
|---|---|---|---|
| DME | 3217 | 31 | 0.96 |
| LED | 1900 | 15 | 0.79 |
| SVO | 2981 | 26 | 0.87 |
| VKO | 1719 | 20 | 1.16 |
Um WHERE status = 'Cancelled' não daria conta disso: ele jogaria fora os outros voos e, com eles, o denominador. A agregação condicional mantém todas as linhas e conta só as que interessam. Repare no 100.0: com 100 e contagens inteiras, o PostgreSQL faria divisão inteira e devolveria 0. No DuckDB, o / sempre devolve um resultado fracionário, então o mesmo bug fica escondido ali até a consulta rodar no PostgreSQL; o DuckDB usa // para divisão inteira.
Há três jeitos equivalentes de contar com condição, e um errado bem tentador:
SELECT COUNT(*) AS arrived,
SUM(CASE WHEN actual_departure - scheduled_departure > INTERVAL '15 minutes' THEN 1 ELSE 0 END) AS late,
COUNT(CASE WHEN actual_departure - scheduled_departure > INTERVAL '15 minutes' THEN 1 END) AS late_count,
ROUND(AVG(CASE WHEN actual_departure - scheduled_departure > INTERVAL '15 minutes' THEN 1.0 ELSE 0 END) * 100, 2) AS late_pct
FROM flights
WHERE status = 'Arrived';
| arrived | late | late_count | late_pct |
|---|---|---|---|
| 16707 | 796 | 796 | 4.76 |
SUM(CASE ... THEN 1 ELSE 0 END)soma 1 para cada correspondência.COUNT(CASE ... THEN 1 END)funciona porqueCOUNT(expressão)ignora NULLs, e semELSEas linhas que não correspondem viram NULL.AVG(CASE ... THEN 1.0 ELSE 0 END)dá a proporção direto: a média de uns e zeros é a fração de uns.
O errado é COUNT(CASE ... THEN 1 ELSE 0 END). Zero não é NULL, então o COUNT conta o zero, e você recebe 16.707: todas as linhas. Se usar COUNT, tire o ELSE; se usar SUM, mantenha o ELSE 0.
FILTER: a sintaxe mais limpa
O PostgreSQL e o DuckDB suportam a cláusula padrão FILTER, que diz a mesma coisa de forma mais direta. Um mapa de assentos por avião, uma coluna por classe:
SELECT aircraft_code,
COUNT(*) FILTER (WHERE fare_conditions = 'Economy') AS economy,
COUNT(*) FILTER (WHERE fare_conditions = 'Comfort') AS comfort,
COUNT(*) FILTER (WHERE fare_conditions = 'Business') AS business
FROM seats
GROUP BY aircraft_code
ORDER BY aircraft_code;
| aircraft_code | economy | comfort | business |
|---|---|---|---|
| 319 | 96 | 0 | 20 |
| 320 | 120 | 0 | 20 |
| 321 | 142 | 0 | 28 |
| 733 | 118 | 0 | 12 |
| 763 | 192 | 0 | 30 |
| 773 | 324 | 48 | 30 |
| CN1 | 12 | 0 | 0 |
| CR2 | 50 | 0 | 0 |
| SU9 | 85 | 0 | 12 |
Isso é um pivot: as linhas de fare_conditions viraram colunas. O FILTER não existe no MySQL, no SQL Server nem no Oracle, então, se o seu código precisa rodar neles, use SUM(CASE ...); funciona em qualquer banco.
Somar valores, não só contar
O CASE dentro do SUM pode devolver o próprio valor em vez de 1. Receita mensal dos voos que chegaram, separada por classe:
SELECT date_trunc('month', f.scheduled_departure)::date AS month,
ROUND(SUM(CASE WHEN tf.fare_conditions = 'Economy' THEN tf.amount ELSE 0 END)) AS economy,
ROUND(SUM(CASE WHEN tf.fare_conditions = 'Comfort' THEN tf.amount ELSE 0 END)) AS comfort,
ROUND(SUM(CASE WHEN tf.fare_conditions = 'Business' THEN tf.amount ELSE 0 END)) AS business
FROM ticket_flights tf
JOIN flights f ON f.flight_id = tf.flight_id
WHERE f.status = 'Arrived'
GROUP BY month
ORDER BY month;
| month | economy | comfort | business |
|---|---|---|---|
| 2026-07-01 | 47444895 | 1899027 | 17733606 |
| 2026-08-01 | 33400255 | 1327303 | 12473544 |
(Agosto é menor porque o retrato dos dados termina em 11 de agosto; o resto do mês ainda não voou.)
E sem ELSE, o AVG calcula a média só das linhas que correspondem, porque as outras são NULL e as agregações ignoram NULLs:
SELECT fare_conditions,
ROUND(SUM(amount)) AS revenue,
ROUND(SUM(CASE WHEN amount >= 600 THEN amount ELSE 0 END)) AS revenue_600plus,
ROUND(AVG(CASE WHEN amount >= 600 THEN amount END), 2) AS avg_600plus
FROM ticket_flights
GROUP BY fare_conditions
ORDER BY revenue DESC;
| fare_conditions | revenue | revenue_600plus | avg_600plus |
|---|---|---|---|
| Economy | 146956844 | 26449215 | 634.9 |
| Business | 55051796 | 31703523 | 1210.94 |
| Comfort | 5661169 | 0 | NULL |
Aqui ELSE 0 e a ausência de ELSE dão resultados diferentes, e os dois estão certos: o SUM precisa dos zeros para que o total seja 0 e não NULL, enquanto o AVG não pode incluir zeros, senão faria a média sobre todos os trechos. A Comfort não tem nenhum trecho de US$ 600 ou mais, então a média dela é NULL: não há nada para calcular.
Tipos: todos os ramos devem devolver o mesmo tipo
Todos os valores de THEN e ELSE precisam caber num único tipo de dado. CASE WHEN ... THEN 1 ELSE 'none' END mistura inteiro com texto. O PostgreSQL recusa com “invalid input syntax for type integer”. O DuckDB também falha, com um erro de conversão, mas só quando alguma linha chega de fato ao ramo 'none', então uma consulta pode passar num teste com uma amostra pequena e quebrar depois. Faça o cast explícito: THEN CAST(x AS VARCHAR), ou devolva NULL em vez de um texto de preenchimento.
CASE no WHERE: em geral, não é necessário
Você pode escrever WHERE CASE WHEN ... THEN ... END, mas um filtro quase sempre fica mais legível com lógica simples de AND / OR. WHERE CASE WHEN status = 'Arrived' THEN actual_departure > '2026-08-01' ELSE false END é só WHERE status = 'Arrived' AND actual_departure > '2026-08-01'. Condições simples também são mais fáceis de o otimizador aproveitar com índices.
Atalhos relacionados
COALESCE(a, b)éCASE WHEN a IS NOT NULL THEN a ELSE b END.NULLIF(a, b)éCASE WHEN a = b THEN NULL ELSE a END; o uso principal é evitar divisão por zero.- O MySQL tem
IF(cond, a, b)e o SQL Server temIIF(cond, a, b). São mais curtos, mas não são portáveis; oCASEroda em qualquer lugar.
Para mais padrões de GROUP BY com contagens condicionais, veja GROUP BY e HAVING. Pivots com agregação condicional também aparecem com frequência nas perguntas de entrevista SQL.
Perguntas frequentes
Qual a diferença entre CASE simples e CASE com condições?
O CASE x WHEN 1 THEN ... simples compara uma expressão com valores por igualdade. O CASE WHEN condição THEN ... aceita qualquer condição: faixas, LIKE, IN, IS NULL, combinações com AND. A forma com condições faz tudo o que a simples faz.
O que o CASE devolve se nenhuma condição for atendida?
O valor do ELSE, ou NULL se não houver ELSE.
Posso usar CASE no WHERE?
Pode, mas raramente é a opção mais clara. A maioria das expressões CASE no WHERE pode ser reescrita como condições comuns de AND / OR.
Como contar linhas que atendem a uma condição no SQL?
Use agregação condicional: SUM(CASE WHEN condição THEN 1 ELSE 0 END), COUNT(CASE WHEN condição THEN 1 END) ou, no PostgreSQL e no DuckDB, COUNT(*) FILTER (WHERE condição). As três mantêm as outras linhas disponíveis para totais e percentuais.
Posso aninhar expressões CASE?
Sim, um THEN ou um ELSE pode conter outro CASE. Mas, em geral, um único CASE com condições combinadas por AND fica mais legível do que o aninhamento.
Outros idiomas: English, Español, Tiếng Việt, 简体中文