SQL para análise de dados · Guias · 30 de outubro de 2026 · 12 min de leitura
Perguntas de entrevista SQL para analista de dados: 20 perguntas com respostas

Uma entrevista de SQL para analista de dados testa duas coisas: se você entende como o SQL avalia uma consulta (perguntas teóricas) e se consegue transformar uma pergunta de negócio numa consulta correta sob pressão de tempo (exercícios ao vivo). Abaixo estão 20 perguntas dos dois tipos, no estilo em que analistas realmente são entrevistados, com respostas-modelo curtas. As respostas teóricas são o que você diria em voz alta; as práticas são as consultas que você escreveria.
Os exercícios usam o banco de dados de uma companhia aérea do curso de SQL da Nomto: airports, aircrafts, seats, flights (33.121 partidas entre julho e setembro de 2026), bookings, tickets, ticket_flights (uma linha por passageiro por trecho de voo, com amount em dólares americanos), boarding_passes e ainda um programa de fidelidade (ticket_members). O retrato dos dados foi tirado em 11 de agosto de 2026 às 18:00. Todas as consultas estão na sintaxe do PostgreSQL e todo resultado mostrado é a saída real; você pode executá-las na sala de SQL da Nomto.
Uma dica que vale para todos os exercícios: antes de escrever, diga em voz alta o que significa uma linha do resultado e o que significa uma linha de cada tabela. A maioria das respostas erradas em entrevista nasce de um desencontro entre as duas coisas.
Perguntas teóricas
1. Em que ordem o SQL avalia um SELECT?
FROM e os JOIN, WHERE, GROUP BY, funções de agregação, HAVING, funções de janela, SELECT (incluindo os aliases), DISTINCT, ORDER BY, LIMIT. Essa ordem explica por que não dá para usar uma agregação no WHERE, por que não dá para filtrar uma função de janela no WHERE e por que o ORDER BY aceita um alias de coluna, mas o WHERE não.
2. Qual a diferença entre WHERE e HAVING?
WHERE filtra linhas antes do agrupamento e não pode usar agregações. HAVING filtra grupos depois da agregação. WHERE status = 'Arrived' decide quais voos entram na contagem; HAVING COUNT(*) > 1000 decide quais aeroportos aparecem no resultado. O passo a passo completo está em GROUP BY e HAVING.
3. Explique INNER, LEFT e FULL OUTER JOIN
INNER JOIN mantém só as linhas que têm correspondência dos dois lados. LEFT JOIN mantém todas as linhas da esquerda e preenche o lado direito com NULL quando não há correspondência. FULL JOIN mantém as linhas sem par dos dois lados. Uma boa resposta acrescenta a consequência: nos nossos dados, aircrafts JOIN flights devolve 8 modelos, porque o Airbus A320-200 não tem voos; aircrafts LEFT JOIN flights devolve os 9. Mais detalhes em JOIN no SQL.
4. COUNT(*), COUNT(coluna) e COUNT(DISTINCT coluna): qual a diferença?
COUNT(*) conta linhas, COUNT(coluna) conta valores não nulos, COUNT(DISTINCT coluna) conta valores não nulos diferentes.
SELECT COUNT(*) AS all_rows,
COUNT(actual_departure) AS departed,
COUNT(DISTINCT flight_no) AS flight_numbers
FROM flights;
| all_rows | departed | flight_numbers |
|---|---|---|
| 33121 | 16773 | 710 |
São 33.121 partidas, das quais 16.773 têm horário real de partida, operadas sob 710 números de voo.
5. Como o NULL se comporta em comparações?
Qualquer comparação com NULL devolve “desconhecido”, não verdadeiro, então WHERE x = NULL não encontra nada:
SELECT COUNT(*) AS flights FROM flights WHERE actual_departure = NULL; -- 0
SELECT COUNT(*) AS flights FROM flights WHERE actual_departure IS NULL; -- 16348
Use IS NULL / IS NOT NULL. As funções de agregação, exceto COUNT(*), ignoram os NULL, e COALESCE(x, 0) os substitui onde o certo é zero.
6. Por que NOT IN com subconsulta é perigoso?
Se a subconsulta devolver um único NULL, x NOT IN (...) nunca pode ser verdadeiro, e a consulta não devolve nenhuma linha. Nos nossos dados, aircrafts WHERE aircraft_code NOT IN (SELECT aircraft_code FROM flights) devolve corretamente 1 modelo. Coloque um único NULL na saída da subconsulta e a mesma consulta devolve 0. Use NOT EXISTS, que não tem essa armadilha.
7. UNION ou UNION ALL
UNION remove duplicados, UNION ALL mantém todas as linhas e é mais barato, porque pula a deduplicação.
SELECT COUNT(*) FROM (
SELECT departure_airport AS airport FROM flights
UNION
SELECT arrival_airport FROM flights
) u;
Isso devolve 104 (aeroportos distintos). Com UNION ALL devolve 66.242, duas linhas por voo. Use UNION ALL, a menos que você precise remover duplicados.
8. ROW_NUMBER, RANK e DENSE_RANK
Todas numeram linhas segundo uma ordenação. Nos empates, ROW_NUMBER continua dando números únicos (1, 2, 3, 4), RANK repete e pula (1, 2, 2, 4), DENSE_RANK repete sem pular (1, 2, 2, 3). Use ROW_NUMBER para “exatamente N linhas” e para deduplicar, DENSE_RANK para “o N-ésimo maior valor”. Exemplos com empates reais estão em funções de janela SQL.
9. O que é chave primária, chave estrangeira e chave composta?
Uma chave primária identifica uma linha de forma única (flights.flight_id). Uma chave estrangeira aponta para a chave de outra tabela (flights.aircraft_code → aircrafts.aircraft_code). Uma chave composta usa várias colunas juntas: em seats, o número de assento 12A se repete entre modelos, então a chave é o par (aircraft_code, seat_no). Na entrevista, complete com o que isso significa para os JOIN: juntar por metade de uma chave composta multiplica linhas.
10. Por que um JOIN às vezes infla um SUM, e como corrigir?
Quando uma linha se junta a várias linhas, ela é repetida, e as suas medidas são somadas uma vez por repetição. As reservas feitas em 2 de agosto somam US$ 4.563.873 em bookings; depois de juntar tickets (vários por reserva), o mesmo SUM(total_amount) dá US$ 7.569.035. Correções: agregar o lado “muitos” antes do JOIN ou somar a medida no seu próprio grão. SUM(DISTINCT ...) não é correção: ele junta reservas diferentes que têm o mesmo valor.
Exercícios ao vivo
11. Encontre o segundo maior alcance de avião sem funções de janela
SELECT MAX(range) AS second_longest
FROM aircrafts
WHERE range < (SELECT MAX(range) FROM aircrafts);
| second_longest |
|---|
| 7900 |
A subconsulta encontra o máximo (11.100 km); a consulta externa pega o máximo de tudo que está abaixo dele. Se o entrevistador pedir o N-ésimo valor em geral, passe para DENSE_RANK (próxima pergunta).
12. O segundo maior preço de trecho em cada classe
WITH p AS (
SELECT DISTINCT fare_conditions, amount FROM ticket_flights
), r AS (
SELECT fare_conditions, amount,
DENSE_RANK() OVER (PARTITION BY fare_conditions ORDER BY amount DESC) AS dr
FROM p
)
SELECT fare_conditions, amount AS second_highest
FROM r
WHERE dr = 2
ORDER BY fare_conditions;
| fare_conditions | second_highest |
|---|---|
| Business | 2013.00 |
| Comfort | 474.00 |
| Economy | 738.00 |
Os preços máximos são US$ 2.033, US$ 476 e US$ 745. DENSE_RANK é a escolha certa: muitos trechos têm o mesmo preço máximo, e ROW_NUMBER devolveria apenas outra cópia dele como “segundo”. Tirar os preços duplicados antes deixa a janela pequena.
13. Mediana do preço por classe
SELECT fare_conditions,
PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY amount) AS median,
ROUND(AVG(amount), 2) AS mean
FROM ticket_flights
GROUP BY fare_conditions
ORDER BY fare_conditions;
| fare_conditions | median | mean |
|---|---|---|
| Business | 350.00 | 511.43 |
| Comfort | 244.00 | 327.41 |
| Economy | 121.00 | 159.6 |
Em todas as classes a média fica bem acima da mediana: uma minoria de rotas longas e caras puxa a média para cima. Dizer isso sem ninguém perguntar é o que transforma uma consulta correta numa boa resposta. PERCENTILE_CONT interpola entre os dois valores do meio; PERCENTILE_DISC devolve um valor que existe nos dados.
14. Encontre duplicados
“Encontre reservas em que o mesmo nome de passageiro aparece em mais de um bilhete.”
SELECT passenger_name, book_ref, COUNT(*) AS tickets
FROM tickets
GROUP BY passenger_name, book_ref
HAVING COUNT(*) > 1
ORDER BY tickets DESC, book_ref
LIMIT 3;
| passenger_name | book_ref | tickets |
|---|---|---|
| ALEKSANDR SOROKIN | 0736ED | 2 |
| TATYANA KUZMINA | 0E7192 | 2 |
| ALEKSANDR VOLKOV | 1697BB | 2 |
Existem 34 pares assim. O ponto que vale levantar: não são necessariamente erros. Duas pessoas na mesma reserva podem ter um nome comum; o ID do passageiro (passenger_id, único em cada bilhete aqui) é a melhor verificação de identidade. Definir o que conta como duplicado faz parte da resposta.
15. Top N por grupo
“Os dois destinos mais frequentes a partir de cada aeroporto de Moscou.”
WITH r AS (
SELECT departure_airport, arrival_airport, COUNT(*) AS flights,
ROW_NUMBER() OVER (PARTITION BY departure_airport
ORDER BY COUNT(*) DESC, arrival_airport) AS rn
FROM flights
WHERE departure_airport IN ('SVO', 'DME', 'VKO')
GROUP BY departure_airport, arrival_airport
)
SELECT departure_airport, arrival_airport, flights, rn
FROM r
WHERE rn <= 2
ORDER BY departure_airport, rn;
| departure_airport | arrival_airport | flights | rn |
|---|---|---|---|
| DME | LED | 244 | 1 |
| DME | BZK | 183 | 2 |
| SVO | LED | 305 | 1 |
| SVO | BZK | 183 | 2 |
| VKO | BZK | 183 | 1 |
| VKO | LED | 183 | 2 |
Comente o empate em Vnukovo: BZK e LED têm 183 voos cada, e só o desempate alfabético decide a ordem. Se as duas linhas empatadas devem contar como primeiras, use RANK.
16. Linhas sem correspondência (anti join)
“Quais modelos de avião nunca voaram?”
SELECT a.aircraft_code, a.model
FROM aircrafts a
WHERE NOT EXISTS (
SELECT 1 FROM flights f WHERE f.aircraft_code = a.aircraft_code
);
| aircraft_code | model |
|---|---|
| 320 | Airbus A320-200 |
O equivalente LEFT JOIN ... WHERE f.flight_id IS NULL também é aceito. Prefira qualquer um dos dois a NOT IN (pergunta 6).
17. Pivot com agregação condicional
“Para cada aeroporto de Moscou, mostre os voos que chegaram, os cancelados e os programados como colunas.”
SELECT departure_airport,
COUNT(*) FILTER (WHERE status = 'Arrived') AS arrived,
COUNT(*) FILTER (WHERE status = 'Cancelled') AS cancelled,
COUNT(*) FILTER (WHERE status = 'Scheduled') AS scheduled
FROM flights
WHERE departure_airport IN ('SVO', 'DME', 'VKO')
GROUP BY departure_airport
ORDER BY departure_airport;
| departure_airport | arrived | cancelled | scheduled |
|---|---|---|---|
| DME | 1618 | 31 | 1510 |
| SVO | 1503 | 26 | 1396 |
| VKO | 869 | 20 | 801 |
Se o banco do entrevistador não tiver FILTER, use SUM(CASE WHEN status = 'Arrived' THEN 1 ELSE 0 END).
18. Total acumulado e variação dia a dia
WITH daily AS (
SELECT CAST(book_date AS date) AS day, COUNT(*) AS bookings
FROM bookings
WHERE book_date >= '2026-08-01' AND book_date < '2026-08-08'
GROUP BY CAST(book_date AS date)
)
SELECT day, bookings,
SUM(bookings) OVER (ORDER BY day) AS running_total,
LAG(bookings) OVER (ORDER BY day) AS prev_day,
bookings - LAG(bookings) OVER (ORDER BY day) AS change
FROM daily
ORDER BY day
LIMIT 4;
| day | bookings | running_total | prev_day | change |
|---|---|---|---|---|
| 2026-08-01 | 5746 | 5746 | NULL | NULL |
| 2026-08-02 | 5761 | 11507 | 5746 | 15 |
| 2026-08-03 | 5665 | 17172 | 5761 | -96 |
| 2026-08-04 | 5813 | 22985 | 5665 | 148 |
Primeiro agregue por dia, depois aplique as janelas sobre as linhas diárias. Um bom comentário extra: se um dia tivesse zero reservas, ele não apareceria em daily, e o LAG compararia com dois dias antes; uma série de datas resolve isso.
19. Porcentagem do total
“Que fatia de todos os voos cada status representa?”
SELECT status, COUNT(*) AS flights,
ROUND(100.0 * COUNT(*) / SUM(COUNT(*)) OVER (), 1) AS pct
FROM flights
GROUP BY status
ORDER BY flights DESC
LIMIT 4;
| status | flights | pct |
|---|---|---|
| Arrived | 16707 | 50.4 |
| Scheduled | 15383 | 46.4 |
| On Time | 518 | 1.6 |
| Cancelled | 414 | 1.2 |
SUM(COUNT(*)) OVER () é o total geral colocado ao lado de cada grupo. O 100.0 evita a divisão inteira.
20. Recompra em até 30 dias
“Que porcentagem dos membros do programa de fidelidade fez outra reserva em até 30 dias depois da primeira?”
WITH mb AS (
SELECT DISTINCT tm.member_id, b.book_ref, b.book_date
FROM ticket_members tm
JOIN tickets t ON t.ticket_no = tm.ticket_no
JOIN bookings b ON b.book_ref = t.book_ref
), firsts AS (
SELECT member_id, MIN(book_date) AS first_booking
FROM mb
GROUP BY member_id
), flags AS (
SELECT f.member_id,
MAX(CASE WHEN mb.book_date > f.first_booking
AND mb.book_date <= f.first_booking + INTERVAL '30 days'
THEN 1 ELSE 0 END) AS repeat_30d
FROM firsts f
JOIN mb ON mb.member_id = f.member_id
WHERE f.first_booking <= TIMESTAMP '2026-08-11 18:00:00' - INTERVAL '30 days'
GROUP BY f.member_id
)
SELECT COUNT(*) AS members, SUM(repeat_30d) AS repeat_30d,
ROUND(100.0 * SUM(repeat_30d) / COUNT(*), 1) AS pct
FROM flags;
| members | repeat_30d | pct |
|---|---|---|
| 16529 | 9843 | 59.5 |
Ou seja, 59,5%. Os três pontos que o entrevistador procura:
- Grão. Um membro pode ter vários bilhetes na mesma reserva, então o
DISTINCTreduz os dados a uma linha por membro por reserva antes de qualquer contagem. - Maturidade. Só entram os membros cuja primeira reserva foi feita pelo menos 30 dias antes do retrato dos dados. Quem reservou pela primeira vez em 5 de agosto ainda não teve 30 dias para voltar; contá-lo como “não voltou” subestimaria a taxa.
- Definição. “Primeira reserva” aqui significa a primeira nos dados, que começam em 17 de junho de 2026. Diga isso: é uma premissa, não um fato sobre o cliente.
Como responder na entrevista
- Reformule a pergunta como uma tabela de resultado: “uma linha por aeroporto, colunas X e Y”. Isso pega mal-entendidos antes de você escrever código.
- Diga o grão de cada tabela que você junta e onde pode haver multiplicação de linhas.
- Trate empates,
NULLe períodos incompletos de forma explícita. São os casos-limite que os entrevistadores plantam de propósito. - Confira o número: compare com um total conhecido ou conte as linhas antes e depois de um
JOIN. - Fale enquanto escreve. Uma consulta parcialmente certa com raciocínio claro costuma valer mais do que uma consulta certa em silêncio.
Para mais exercícios práticos, do fácil ao difícil, veja exercícios de SQL com respostas.
Perguntas frequentes
Quais temas de SQL caem em entrevistas para analista de dados?
Os mais comuns: GROUP BY com HAVING, JOIN (incluindo LEFT JOIN e anti join), tratamento de NULL, subconsultas e CTEs, funções de janela (ranking, top N por grupo, total acumulado, LAG/LEAD), porcentagem do total e variação entre períodos. Vagas mais seniores acrescentam retenção, funis e verificações de qualidade dos dados.
Qual dialeto de SQL usar na entrevista?
Pergunte, mas um SQL padrão próximo do PostgreSQL é uma escolha segura. Se você usar um recurso específico de um dialeto, como FILTER ou QUALIFY, mencione a alternativa portátil.
Como encontrar o N-ésimo maior valor em SQL?
Use DENSE_RANK() OVER (ORDER BY valor DESC) numa subconsulta e filtre = N; acrescente PARTITION BY para “por grupo”. Só para o segundo maior, MAX(valor) WHERE valor < (SELECT MAX(valor) ...) funciona sem funções de janela.
Como se preparar para exercícios de SQL ao vivo?
Pratique num banco de dados realista com várias tabelas em vez de quebra-cabeças de uma tabela só, cronometre o seu tempo e sempre confira o resultado com um número independente. Todos os exercícios acima podem ser executados e conferidos na sala de SQL da Nomto.
Outros idiomas: Tiếng Việt, English, Español, 简体中文