SQL para análise de dados · Guias · 30 de outubro de 2026 · 12 min de leitura
Exercícios de SQL com respostas: 15 exercícios do básico ao avançado

Aqui estão 15 exercícios de SQL com respostas, todos sobre o mesmo banco de dados e organizados do mais fácil ao mais difícil: primeiro SELECT, WHERE e ORDER BY, depois GROUP BY e HAVING, em seguida datas, condições e subconsultas, e no fim JOIN no grão certo e funções de janela. Cada exercício traz o enunciado, a consulta em “Solução”, o resultado real e uma ou duas linhas explicando por que a solução funciona. Tente resolver cada um antes de olhar a resposta: o que ensina SQL é o minuto em que você fica travado, não a leitura.
Todos os exercícios usam o banco de dados de uma companhia aérea do curso de SQL da Nomto. É um esquema no estilo PostgreSQL com volumes reais: 104 aeroportos, 9 modelos de avião, 33.121 voos de 12 de julho a 10 de setembro de 2026, 262.788 reservas e 366.733 bilhetes. Os horários estão no horário-base da companhia (Moscou, UTC+3), os valores em dólares americanos e os textos dos dados em inglês ('Moscow', 'Economy', 'Arrived'). O retrato dos dados foi tirado em 11 de agosto de 2026 às 18:00: os voos antes desse momento já aconteceram, os posteriores estão apenas programados. Você pode executar todas as consultas na sala de SQL da Nomto e comparar a sua saída com as tabelas daqui.
As tabelas que você vai usar
| Tabela | Uma linha é | Colunas principais |
|---|---|---|
airports |
um aeroporto | airport_code, airport_name, city, timezone, longitude |
aircrafts |
um modelo de avião | aircraft_code, model, range (km) |
seats |
um assento na cabine de um modelo | aircraft_code, seat_no, fare_conditions |
flights |
uma partida | flight_id, flight_no, scheduled_departure, scheduled_arrival, departure_airport, arrival_airport, status, aircraft_code, actual_departure, actual_arrival |
bookings |
uma reserva | book_ref, book_date, total_amount |
tickets |
o bilhete de um passageiro | ticket_no, book_ref, passenger_name |
ticket_flights |
um bilhete em um trecho de voo | ticket_no, flight_id, fare_conditions, amount |
boarding_passes |
o cartão de embarque de um trecho | ticket_no, flight_id, seat_no |
Fácil: SELECT, WHERE, ORDER BY
1. Quantos aeroportos e quantas cidades existem?
Solução
SELECT COUNT(*) AS airports, COUNT(DISTINCT city) AS cities
FROM airports;
| airports | cities |
|---|---|
| 104 | 101 |
COUNT(*) conta linhas, COUNT(DISTINCT city) conta valores diferentes. Três cidades a menos que aeroportos significa que algumas cidades têm mais de um aeroporto (o exercício 6 encontra quais).
2. Liste os modelos de avião com alcance de pelo menos 5.000 km, do maior para o menor
Solução
SELECT model, range
FROM aircrafts
WHERE range >= 5000
ORDER BY range DESC;
| model | range |
|---|---|
| Boeing 777-300 | 11100 |
| Boeing 767-300 | 7900 |
| Airbus A319-100 | 6700 |
| Airbus A320-200 | 5700 |
| Airbus A321-200 | 5600 |
“Pelo menos” significa >=. Um deslize comum é usar >, que aqui daria a mesma resposta só por sorte, porque nenhum modelo tem exatamente 5.000 km.
3. Os cinco aeroportos mais a leste fora do horário de Moscou
Solução
SELECT airport_code, airport_name, city, timezone
FROM airports
WHERE timezone <> 'Europe/Moscow'
ORDER BY longitude DESC
LIMIT 5;
| airport_code | airport_name | city | timezone |
|---|---|---|---|
| DYR | Anadyr | Anadyr | Asia/Anadyr |
| PKC | Yelizovo | Petropavlovsk-Kamchatsky | Asia/Kamchatka |
| GDX | Magadan | Magadan | Asia/Magadan |
| UUS | Khomutovo | Yuzhno-Sakhalinsk | Asia/Sakhalin |
| KXK | Khurba | Komsomolsk-on-Amur | Asia/Vladivostok |
“Mais a leste” é a maior longitude. Dos 104 aeroportos, 60 ficam fora do horário de Moscou.
Fácil a médio: GROUP BY e HAVING
4. Quantos voos partem em cada dia da semana?
Use quatro semanas completas, de 13 de julho a 9 de agosto, para que cada dia da semana apareça o mesmo número de vezes.
Solução
SELECT EXTRACT(ISODOW FROM scheduled_departure) AS weekday, COUNT(*) AS flights
FROM flights
WHERE scheduled_departure >= '2026-07-13'
AND scheduled_departure < '2026-08-10'
GROUP BY EXTRACT(ISODOW FROM scheduled_departure)
ORDER BY weekday;
| weekday | flights |
|---|---|
| 1 | 2144 |
| 2 | 2180 |
| 3 | 2192 |
| 4 | 2200 |
| 5 | 1972 |
| 6 | 2272 |
| 7 | 2232 |
ISODOW numera a segunda-feira como 1 e o domingo como 7. O sábado é o dia mais movimentado, a sexta-feira o mais tranquilo. Comparar dias da semana num período que não é um número inteiro de semanas daria a alguns dias uma semana extra de voos.
5. As cinco rotas mais movimentadas em número de voos
Solução
SELECT departure_airport, arrival_airport, COUNT(*) AS flights
FROM flights
GROUP BY departure_airport, arrival_airport
ORDER BY flights DESC, departure_airport, arrival_airport
LIMIT 5;
| departure_airport | arrival_airport | flights |
|---|---|---|
| LED | SVO | 305 |
| SVO | LED | 305 |
| DME | LED | 244 |
| LED | DME | 244 |
| BZK | DME | 183 |
Uma rota é um par de aeroportos, então os dois entram no GROUP BY. As colunas extras de ordenação deixam a ordem estável quando há empate na contagem.
6. Quais cidades têm mais de um aeroporto?
Solução
SELECT city, COUNT(*) AS airports
FROM airports
GROUP BY city
HAVING COUNT(*) > 1
ORDER BY airports DESC;
| city | airports |
|---|---|
| Moscow | 3 |
| Ulyanovsk | 2 |
A condição é sobre uma contagem, então vai no HAVING, não no WHERE. A diferença entre os dois está explicada em GROUP BY e HAVING.
7. Quais modelos de avião têm mais de 200 assentos?
Solução
SELECT a.model, COUNT(*) AS seats
FROM aircrafts a
JOIN seats s ON s.aircraft_code = a.aircraft_code
GROUP BY a.model
HAVING COUNT(*) > 200
ORDER BY seats DESC;
| model | seats |
|---|---|
| Boeing 777-300 | 402 |
| Boeing 767-300 | 222 |
seats tem uma linha por assento, então contar linhas por modelo dá o tamanho da cabine; o JOIN só acrescenta o nome legível do modelo.
8. Receita, trechos e preço médio por classe
Solução
SELECT fare_conditions,
COUNT(*) AS segments,
SUM(amount) AS revenue,
ROUND(AVG(amount), 2) AS avg_price
FROM ticket_flights
GROUP BY fare_conditions
ORDER BY revenue DESC;
| fare_conditions | segments | revenue | avg_price |
|---|---|---|---|
| Economy | 920793 | 146956844.00 | 159.6 |
| Business | 107642 | 55051796.00 | 511.43 |
| Comfort | 17291 | 5661169.00 | 327.41 |
A receita por classe está em ticket_flights, uma linha por passageiro por trecho. Somar bookings.total_amount depois de juntar a tabela aos trechos contaria cada reserva várias vezes.
Médio: datas, condições, subconsultas
9. Os voos programados mais longos
Solução
SELECT DISTINCT flight_no, departure_airport, arrival_airport,
EXTRACT(EPOCH FROM scheduled_arrival - scheduled_departure) / 60 AS minutes
FROM flights
ORDER BY minutes DESC, flight_no
LIMIT 3;
| flight_no | departure_airport | arrival_airport | minutes |
|---|---|---|---|
| PG0144 | DME | PKC | 530 |
| PG0145 | PKC | DME | 530 |
| PG0168 | DME | UUS | 530 |
Subtrair dois timestamps dá um intervalo; EXTRACT(EPOCH FROM ...) o converte em segundos. Sem DISTINCT, o mesmo número de voo ocuparia as primeiras linhas uma vez para cada dia em que voa. Vários voos empatam em 8 h 50 min, então LIMIT 3 mostra só alguns deles.
10. Que porcentagem dos voos que chegaram pousou com mais de uma hora de atraso?
Solução
SELECT COUNT(*) AS arrived,
COUNT(*) FILTER (WHERE actual_arrival - scheduled_arrival > INTERVAL '1 hour') AS late_1h,
ROUND(100.0 * COUNT(*) FILTER (WHERE actual_arrival - scheduled_arrival > INTERVAL '1 hour')
/ COUNT(*), 2) AS pct
FROM flights
WHERE status = 'Arrived';
| arrived | late_1h | pct |
|---|---|---|
| 16707 | 796 | 4.76 |
Ou seja, 4,76%. FILTER (WHERE ...) conta só as linhas que atendem à condição, na mesma passada que o total. SUM(CASE WHEN ... THEN 1 ELSE 0 END) faz o mesmo em bancos sem FILTER. Multiplique por 100.0, e não por 100, para evitar a divisão inteira.
11. Quantas reservas estão acima do valor médio de reserva?
Solução
SELECT COUNT(*) AS above_avg
FROM bookings
WHERE total_amount > (SELECT AVG(total_amount) FROM bookings);
| above_avg |
|---|
| 87224 |
A subconsulta devolve um único número (a média é US$ 790,26), que o WHERE externo compara com cada linha. Só cerca de um terço das 262.788 reservas fica acima da média: algumas reservas grandes puxam a média para cima.
12. As três rotas com maior receita de bilhetes
Solução
SELECT f.departure_airport, f.arrival_airport, SUM(tf.amount) AS revenue
FROM ticket_flights tf
JOIN flights f ON f.flight_id = tf.flight_id
GROUP BY f.departure_airport, f.arrival_airport
ORDER BY revenue DESC
LIMIT 3;
| departure_airport | arrival_airport | revenue |
|---|---|---|
| DME | KHV | 7534783.00 |
| KHV | DME | 7337978.00 |
| DME | OVB | 5482189.00 |
Cada trecho pertence a um único voo, então esse JOIN nunca multiplica linhas: a soma continua no grão do trecho. Moscou-Domodedovo para Khabarovsk é a rota que mais fatura, e não a mais frequente do exercício 5. Frequência e dinheiro são rankings diferentes.
Avançado: JOIN no grão certo e funções de janela
13. Taxa de ocupação por modelo de avião nos voos que chegaram
Taxa de ocupação = passageiros embarcados / assentos oferecidos.
Solução
WITH cap AS (
SELECT aircraft_code, COUNT(*) AS seats
FROM seats
GROUP BY aircraft_code
), bp AS (
SELECT flight_id, COUNT(*) AS boarded
FROM boarding_passes
GROUP BY flight_id
)
SELECT f.aircraft_code,
COUNT(*) AS flights,
ROUND(100.0 * SUM(COALESCE(bp.boarded, 0)) / SUM(cap.seats), 1) AS load_pct
FROM flights f
JOIN cap ON cap.aircraft_code = f.aircraft_code
LEFT JOIN bp ON bp.flight_id = f.flight_id
WHERE f.status = 'Arrived'
GROUP BY f.aircraft_code
ORDER BY load_pct DESC;
| aircraft_code | flights | load_pct |
|---|---|---|
| 773 | 307 | 65.8 |
| 733 | 646 | 56.4 |
| 763 | 612 | 50.2 |
| SU9 | 4292 | 48.1 |
| 319 | 624 | 40.3 |
| CR2 | 4570 | 36.5 |
| 321 | 982 | 34.2 |
| CN1 | 4674 | 14.4 |
Tanto os assentos quanto os cartões de embarque são agregados para uma linha por modelo e por voo antes do JOIN. Juntar seats e boarding_passes brutos aos voos multiplicaria uma tabela pela outra. LEFT JOIN com COALESCE mantém os voos sem ninguém a bordo como zero, em vez de descartá-los. O guia de JOIN no SQL explica essa multiplicação de linhas em detalhe.
14. A rota que mais fatura para cada modelo de avião
Solução
WITH r AS (
SELECT f.aircraft_code, f.departure_airport, f.arrival_airport,
SUM(tf.amount) AS revenue,
ROW_NUMBER() OVER (PARTITION BY f.aircraft_code ORDER BY SUM(tf.amount) DESC) AS rn
FROM ticket_flights tf
JOIN flights f ON f.flight_id = tf.flight_id
GROUP BY f.aircraft_code, f.departure_airport, f.arrival_airport
)
SELECT aircraft_code, departure_airport, arrival_airport, revenue
FROM r
WHERE rn = 1
ORDER BY revenue DESC;
| aircraft_code | departure_airport | arrival_airport | revenue |
|---|---|---|---|
| 763 | DME | KHV | 7534783.00 |
| 773 | DME | OVB | 5482189.00 |
| 321 | LED | IKT | 4116030.00 |
| 319 | SVO | UUS | 2365732.00 |
| 733 | SVO | MJZ | 1494478.00 |
| SU9 | DME | NSK | 1347231.00 |
| CR2 | DME | NJC | 449695.00 |
| CN1 | PEE | ARH | 50600.00 |
A janela é calculada depois do GROUP BY, por isso pode ordenar por SUM(tf.amount). PARTITION BY reinicia a numeração para cada modelo, e o filtro rn = 1 precisa ficar fora, porque uma função de janela não pode ser usada no WHERE. Veja funções de janela SQL para as alternativas RANK e DENSE_RANK quando os empates importam.
15. Variação semanal das reservas e a armadilha da última semana
Solução
WITH w AS (
SELECT DATE_TRUNC('week', book_date) AS week, COUNT(*) AS bookings
FROM bookings
WHERE book_date >= '2026-07-13'
GROUP BY DATE_TRUNC('week', book_date)
)
SELECT CAST(week AS date) AS week,
bookings,
LAG(bookings) OVER (ORDER BY week) AS prev_week,
ROUND(100.0 * (bookings - LAG(bookings) OVER (ORDER BY week))
/ LAG(bookings) OVER (ORDER BY week), 1) AS change_pct
FROM w
ORDER BY week;
| week | bookings | prev_week | change_pct |
|---|---|---|---|
| 2026-07-13 | 38490 | NULL | NULL |
| 2026-07-20 | 38728 | 38490 | 0.6 |
| 2026-07-27 | 39216 | 38728 | 1.3 |
| 2026-08-03 | 42561 | 39216 | 8.5 |
| 2026-08-10 | 12122 | 42561 | -71.5 |
DATE_TRUNC('week', ...) leva cada timestamp para a segunda-feira da sua semana; LAG traz a semana anterior para a linha atual. A consulta está correta, mas a última linha não é uma queda de 71,5%: os dados terminam na terça-feira, 11 de agosto, às 18:00, então essa semana tem menos de dois dias. Antes de reportar uma variação, confira se os dois períodos estão completos; aqui a resposta honesta é “+8,5% na última semana completa”.
Como aproveitar estes exercícios
- Escreva primeiro, compare depois. A sua consulta pode ser diferente e mesmo assim estar certa. Compare resultados, não sintaxe.
- Confira a contagem de linhas. Antes de confiar num
SUMdepois de umJOIN, compare oCOUNT(*)com o que você espera a partir do grão das tabelas. - Adicione um critério de desempate a todo
ORDER BY ... LIMITque possa empatar, para que a saída seja reproduzível. - Pergunte se o período está completo sempre que comparar datas perto do limite dos dados.
Se você quer variações destes exercícios no formato de entrevista, com respostas-modelo, veja perguntas de entrevista SQL para analista de dados.
Perguntas frequentes
Para qual banco de dados são estes exercícios de SQL?
Eles estão escritos na sintaxe do PostgreSQL, sobre um banco de dados de companhia aérea (voos, reservas, bilhetes, cartões de embarque) usado no curso de SQL da Nomto. Fora FILTER e ISODOW, as consultas rodam com pouca ou nenhuma mudança na maioria dos bancos SQL.
Minha consulta dá o mesmo resultado, mas é diferente. Está errada?
Não necessariamente. Muitos exercícios têm várias soluções corretas, por exemplo FILTER ou CASE, subconsulta ou CTE. Se o seu resultado bate nos dados completos, incluindo casos-limite como empates e grupos vazios, a consulta está certa.
Em que ordem estudar SQL para iniciantes?
A ordem desta lista funciona: filtrar e ordenar, depois funções de agregação e GROUP BY/HAVING, depois JOIN, depois subconsultas e CTEs, e por fim funções de janela. JOIN e agregação juntos causam a maior parte dos erros do dia a dia, então dedique mais tempo aos exercícios 8, 12 e 13.
Estes exercícios parecem com os de entrevista de SQL?
Os exercícios 6 e 9 a 15 são próximos do que se pergunta em entrevistas para analista: agrupamento com condições, JOIN sem contagem dupla, top N por grupo e variação entre períodos.
Outros idiomas: Tiếng Việt, English, Español, 简体中文