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

Um caderno com uma lista de tarefas marcadas ao lado de um pequeno avião de brinquedo e de um notebook com uma tabela de dados

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

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, 简体中文