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

Duas pessoas numa mesa de entrevista, uma apontando para um notebook com um diagrama de banco de dados, ao lado de um café e de um cartão de embarque

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:

  1. Grão. Um membro pode ter vários bilhetes na mesma reserva, então o DISTINCT reduz os dados a uma linha por membro por reserva antes de qualquer contagem.
  2. 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.
  3. 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

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