SQL para análise de dados · Guias · 30 de outubro de 2026 · 14 min de leitura

Subconsulta SQL: subquery no WHERE, no SELECT e no FROM, com exemplos

Um pequeno painel de partidas encaixado dentro de um painel de partidas maior de aeroporto

Uma subconsulta (subquery) é um SELECT escrito dentro de outra consulta, entre parênteses. A consulta externa usa o resultado dela como usaria um valor, uma lista ou uma tabela. Isso dá três lugares onde as subconsultas aparecem e três formatos que elas assumem:

Além disso, uma subconsulta é independente (roda uma vez e não depende da linha externa) ou correlacionada (faz referência a uma coluna da consulta externa e, logicamente, é reavaliada para cada linha externa). Quando você consegue distinguir esses casos, quase toda subconsulta fica fácil de ler.

Todos 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, 262.788 reservas, bilhetes, trechos de voo 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 abaixo saiu da execução real da consulta.

Subconsulta no WHERE: comparar com um valor único

A primeira subconsulta clássica: quais reservas estão acima do valor médio de reserva? Você não pode escrever WHERE total_amount > AVG(total_amount), porque agregações não são permitidas no WHERE. Então você calcula a média numa subconsulta:

SELECT COUNT(*) AS bookings_above_avg
FROM bookings
WHERE total_amount > (SELECT AVG(total_amount) FROM bookings);
bookings_above_avg
87224

A consulta interna devolve exatamente uma linha e uma coluna: US$ 790,26. Isso é uma subconsulta escalar, e o SQL deixa você colocá-la em qualquer lugar onde um valor único é permitido. O banco a executa uma vez e depois compara cada reserva com esse número. Cerca de um terço das 262.788 reservas fica acima da média, o que é normal para dinheiro: poucas reservas grandes puxam a média para cima.

O mesmo padrão encontra a linha que tem um valor extremo:

SELECT book_ref, book_date, total_amount
FROM bookings
WHERE total_amount = (SELECT MAX(total_amount) FROM bookings);
book_ref book_date total_amount
3B54BB 2026-07-01 17:08:00 12045.00

Se duas reservas empatassem no máximo, você receberia as duas, o que muitas vezes é o que você quer e é a diferença em relação a ORDER BY total_amount DESC LIMIT 1.

IN: comparar com uma lista

Quando a subconsulta devolve uma coluna, mas muitas linhas, use IN. Quais modelos de avião decolam de Sheremetyevo (SVO)?

SELECT aircraft_code, model
FROM aircrafts
WHERE aircraft_code IN (
  SELECT aircraft_code FROM flights WHERE departure_airport = 'SVO'
)
ORDER BY aircraft_code;
aircraft_code model
319 Airbus A319-100
321 Airbus A321-200
733 Boeing 737-300
763 Boeing 767-300
773 Boeing 777-300
CN1 Cessna 208 Caravan
CR2 Bombardier CRJ-200
SU9 Sukhoi Superjet-100

A consulta interna devolve 2.981 códigos, um por partida de SVO, com muitas repetições. O IN não se importa: cada modelo aparece ou não aparece na lista, então cada modelo sai uma vez só.

Compare com o join que você talvez escrevesse no lugar:

SELECT COUNT(*) AS rows_returned, COUNT(DISTINCT a.aircraft_code) AS models
FROM aircrafts a
JOIN flights f ON f.aircraft_code = a.aircraft_code
WHERE f.departure_airport = 'SVO';
rows_returned models
2981 8

O join devolve uma linha por voo correspondente, então você precisaria de DISTINCT para voltar aos oito modelos. Essa é a principal diferença prática entre os dois: uma subconsulta no WHERE filtra linhas sem multiplicá-las; um join pode multiplicá-las. Quando você só precisa saber se existe correspondência, uma subconsulta (ou EXISTS, mais abaixo) é a ferramenta mais limpa. O guia de JOIN no SQL trata dessa multiplicação em detalhe.

Uma subconsulta de lista pode ter o próprio GROUP BY e HAVING. Quais cidades têm mais de um aeroporto, e quantos?

SELECT city, COUNT(*) AS airports
FROM airports
WHERE city IN (
  SELECT city FROM airports GROUP BY city HAVING COUNT(*) > 1
)
GROUP BY city
ORDER BY city;
city airports
Moscow 3
Ulyanovsk 2

ANY e ALL

IN é uma abreviação de = ANY (...). As formas ANY e ALL também funcionam com >, < e as outras comparações. Quais aviões voam mais longe que todos os Airbus da frota?

SELECT model, range
FROM aircrafts
WHERE range > ALL (SELECT range FROM aircrafts WHERE model LIKE 'Airbus%')
ORDER BY range DESC;
model range
Boeing 777-300 11100
Boeing 767-300 7900

> ALL (...) é o mesmo que > (SELECT MAX(...)), e a maioria das pessoas acha a versão com MAX mais fácil de ler. Você vai encontrar ANY e ALL mais em código antigo e em perguntas de entrevista do que em análises novas.

EXISTS e NOT EXISTS: existe pelo menos uma linha correspondente?

EXISTS (subconsulta) é verdadeiro se a subconsulta devolve pelo menos uma linha. O que a subconsulta seleciona não importa, e é por isso que se escreve SELECT 1. O EXISTS é quase sempre correlacionado: a consulta interna faz referência à linha externa.

Quantos membros do programa de fidelidade nunca compraram um bilhete?

SELECT COUNT(*) AS members_without_tickets
FROM loyalty_members m
WHERE NOT EXISTS (
  SELECT 1 FROM ticket_members tm WHERE tm.member_id = m.member_id
);
members_without_tickets
11426

Leia linha por linha: para cada membro m, procure algum bilhete ligado a m.member_id; mantenha o membro se não houver nenhum. Dos 53.000 membros, 11.426 entraram no programa, mas não têm bilhetes nestes dados.

O EXISTS também resolve condições que ficam a duas tabelas de distância. Quantas reservas incluem pelo menos um trecho em classe executiva?

SELECT COUNT(*) AS bookings_with_business
FROM bookings b
WHERE EXISTS (
  SELECT 1
  FROM tickets t
  JOIN ticket_flights tf ON tf.ticket_no = t.ticket_no
  WHERE t.book_ref = b.book_ref
    AND tf.fare_conditions = 'Business'
);
bookings_with_business
83730

Uma reserva com cinco trechos executivos é contada uma vez. Escrever a mesma coisa com join exigiria COUNT(DISTINCT b.book_ref) para desfazer as duplicatas.

A armadilha do NOT IN

NOT EXISTS e NOT IN parecem intercambiáveis e, em geral, dão a mesma resposta. Qual modelo de avião nunca aparece na programação?

SELECT aircraft_code
FROM aircrafts
WHERE aircraft_code NOT IN (SELECT aircraft_code FROM flights);
aircraft_code
320

O Airbus A320 está na frota, mas não faz nenhum voo nesta programação. Funcionou porque flights.aircraft_code não tem NULL. Agora tente o mesmo padrão numa coluna que tem. Em app_events, member_id é NULL para visitantes que não fizeram login. Quais membros do programa de fidelidade não têm eventos no app?

SELECT COUNT(*) AS members_without_events
FROM loyalty_members
WHERE member_id NOT IN (SELECT member_id FROM app_events);
members_without_events
0

Zero, e está errado. A versão com NOT EXISTS dá a resposta real:

SELECT COUNT(*) AS members_without_events
FROM loyalty_members m
WHERE NOT EXISTS (
  SELECT 1 FROM app_events e WHERE e.member_id = m.member_id
);
members_without_events
19376

x NOT IN (lista) significa “x é diferente de todos os elementos”. Se a lista contém um NULL, a comparação x <> NULL é desconhecida, então a condição inteira nunca é verdadeira e todas as linhas são descartadas. A consulta interna aqui devolve 912.625 NULLs. A correção é NOT EXISTS ou NOT IN (SELECT member_id FROM app_events WHERE member_id IS NOT NULL), que devolve os mesmos 19.376. O hábito que vale criar: use NOT EXISTS para “linhas sem correspondência”. O guia de NULL no SQL explica a lógica de três valores por trás disso.

Subconsultas correlacionadas: comparar cada linha com o próprio grupo

Uma subconsulta escalar também pode ser correlacionada. Quantos trechos custam mais que a média da própria classe?

SELECT tf.fare_conditions, COUNT(*) AS segments
FROM ticket_flights tf
WHERE tf.amount > (
  SELECT AVG(t2.amount)
  FROM ticket_flights t2
  WHERE t2.fare_conditions = tf.fare_conditions
)
GROUP BY tf.fare_conditions
ORDER BY segments DESC;
fare_conditions segments
Economy 291102
Business 29126
Comfort 6919

A consulta interna usa tf.fare_conditions da linha externa, então a média que ela devolve depende dessa linha: US$ 159,60 para Economy, US$ 327,41 para Comfort, US$ 511,43 para Business. Dê aliases diferentes às duas cópias da tabela (tf e t2); senão não dá para dizer a qual você está se referindo.

“Logicamente reavaliada para cada linha” não quer dizer que o banco executa literalmente a consulta interna um milhão de vezes. O PostgreSQL e o DuckDB reescrevem muitas subconsultas correlacionadas como joins. Mesmo assim, se uma subconsulta correlacionada estiver lenta, a mesma lógica costuma ficar mais clara como função de janela: amount > AVG(amount) OVER (PARTITION BY fare_conditions) numa tabela derivada. Veja funções de janela SQL.

Subconsulta no SELECT: um valor por linha

Uma subconsulta escalar na lista do SELECT acrescenta um valor calculado a cada linha de saída. Quantos assentos tem cada modelo, e quantos voos ele opera?

SELECT a.model,
       (SELECT COUNT(*) FROM seats s WHERE s.aircraft_code = a.aircraft_code) AS seats,
       (SELECT COUNT(*) FROM flights f WHERE f.aircraft_code = a.aircraft_code) AS flights
FROM aircrafts a
ORDER BY seats DESC;
model seats flights
Boeing 777-300 402 610
Boeing 767-300 222 1221
Airbus A321-200 170 1952
Airbus A320-200 140 0
Boeing 737-300 130 1274
Airbus A319-100 116 1239
Sukhoi Superjet-100 97 8504
Bombardier CRJ-200 50 9048
Cessna 208 Caravan 12 9273

É um jeito elegante de colocar lado a lado duas contagens de duas tabelas diferentes. Juntar aircrafts com seats e com flights e agrupar multiplicaria assentos por voos e daria um número sem sentido. Repare também que o A320 recebe 0, e não some do resultado: COUNT(*) sobre nenhuma linha é 0.

Uma subconsulta escalar não correlacionada no SELECT é prática para participação no total:

SELECT model,
       (SELECT COUNT(*) FROM seats s WHERE s.aircraft_code = a.aircraft_code) AS seats,
       ROUND(100.0 * (SELECT COUNT(*) FROM seats s WHERE s.aircraft_code = a.aircraft_code)
             / (SELECT COUNT(*) FROM seats), 1) AS pct_of_all_seats
FROM aircrafts a
ORDER BY seats DESC
LIMIT 4;
model seats pct_of_all_seats
Boeing 777-300 402 30
Boeing 767-300 222 16.6
Airbus A321-200 170 12.7
Airbus A320-200 140 10.5

Funciona, mas a contagem de assentos agora está escrita duas vezes. Quando você vê a mesma subconsulta repetida, é a hora de levá-la para uma CTE ou para uma tabela derivada.

Subconsulta no FROM: uma tabela derivada

Uma subconsulta no FROM devolve uma tabela inteira, e a consulta externa a trata como qualquer outra tabela. É assim que você agrega duas vezes: primeiro por reserva, depois entre as reservas. Quantos bilhetes tem uma reserva típica?

SELECT tickets_in_booking, COUNT(*) AS bookings
FROM (
  SELECT book_ref, COUNT(*) AS tickets_in_booking
  FROM tickets
  GROUP BY book_ref
) AS per_booking
GROUP BY tickets_in_booking
ORDER BY tickets_in_booking;
tickets_in_booking bookings
1 173390
2 75793
3 12686
4 896
5 23

A consulta interna produz uma linha por reserva com a contagem de bilhetes. A consulta externa agrupa essas linhas de novo. Não dá para fazer isso num único GROUP BY, porque COUNT(COUNT(*)) não é permitido. A mesma tabela derivada dá a média, 1,396 bilhete por reserva, com SELECT ROUND(AVG(tickets_in_booking), 3) FROM (...) AS per_booking.

Tabelas derivadas também se combinam com joins. Aqui as partidas são contadas primeiro por aeroporto e só depois ligadas a airports para pegar o nome da cidade:

SELECT a.city, d.departures
FROM (
  SELECT departure_airport, COUNT(*) AS departures
  FROM flights
  WHERE status = 'Arrived'
  GROUP BY departure_airport
) AS d
JOIN airports a ON a.airport_code = d.departure_airport
ORDER BY d.departures DESC
LIMIT 5;
city departures
Moscow 1618
Moscow 1503
St. Petersburg 956
Moscow 869
Novosibirsk 536

Moscou aparece três vezes porque tem três aeroportos (DME, SVO, VKO), e o agrupamento foi feito por aeroporto. Se você quer uma linha por cidade, agrupe por a.city depois do join: aí Moscou tem 3.990 partidas. Nenhuma das duas está errada; a subconsulta deixa evidente em que nível você agregou.

LATERAL: uma subconsulta no FROM que enxerga a linha externa

Uma tabela derivada comum não pode fazer referência a outras tabelas do mesmo FROM. O LATERAL tira essa restrição, o que deixa o “top N por grupo” fácil de expressar. Os dois destinos mais frequentes a partir de SVO e de LED:

SELECT ap.airport_code, ap.city, top.arrival_airport, top.flights
FROM airports ap
CROSS JOIN LATERAL (
  SELECT f.arrival_airport, COUNT(*) AS flights
  FROM flights f
  WHERE f.departure_airport = ap.airport_code
  GROUP BY f.arrival_airport
  ORDER BY flights DESC, f.arrival_airport
  LIMIT 2
) AS top
WHERE ap.airport_code IN ('SVO', 'LED')
ORDER BY ap.airport_code, top.flights DESC;
airport_code city arrival_airport flights
LED St. Petersburg SVO 305
LED St. Petersburg DME 244
SVO Moscow LED 305
SVO Moscow BZK 183

O LATERAL funciona no PostgreSQL, no DuckDB e no MySQL 8; o SQL Server escreve CROSS APPLY. Para a maioria dos analistas, a abordagem de top N com ROW_NUMBER() é mais comum, mas vale reconhecer o LATERAL.

Subconsulta, JOIN ou CTE?

Você precisa Em geral, o melhor
Filtrar por “tem correspondência” / “não tem correspondência” EXISTS / NOT EXISTS
Filtrar por uma lista curta ou por um único valor calculado IN / subconsulta escalar no WHERE
Colunas de outra tabela na saída JOIN
Agregar e depois agregar de novo tabela derivada ou CTE
O mesmo resultado intermediário usado duas vezes, ou mais de dois níveis de aninhamento CTE
Um valor por linha vindo de outra tabela, sem multiplicar linhas subconsulta escalar no SELECT ou join pré-agregado

Desempenho raramente é o fator decisivo: os otimizadores modernos transformam IN, EXISTS e muitas subconsultas correlacionadas em joins internamente. Escolha a forma que expressa a pergunta de modo mais direto e, se ficar lento, confira o plano de execução.

Erros comuns

1. Uma subconsulta escalar que devolve mais de uma linha

SELECT (SELECT aircraft_code FROM flights WHERE departure_airport = 'SVO') AS x;

O PostgreSQL para com a mensagem “more than one row returned by a subquery used as an expression”, e o DuckDB com uma mensagem parecida. Uma subconsulta usada como valor único deve devolver no máximo uma linha: agregue (MAX, COUNT), filtre até sobrar uma linha ou troque por IN se você queria mesmo uma lista. Se ela não devolver nenhuma linha, o valor é NULL, o que pode esvaziar um WHERE em silêncio.

2. NOT IN sobre uma coluna com NULL

Já visto acima: um NULL na lista faz o NOT IN não devolver nada. Use NOT EXISTS.

3. Uma coluna sem qualificação que se refere, em silêncio, à consulta externa

SELECT COUNT(*) AS models
FROM aircrafts
WHERE aircraft_code IN (SELECT aircraft_code FROM airports);
models
9

A tabela airports não tem coluna aircraft_code, mas a consulta roda e devolve os nove modelos. O SQL procurou aircraft_code na tabela interna, não encontrou e pegou da linha externa de aircrafts, então a condição virou “aircraft_code é igual a ele mesmo”. Sempre qualifique as colunas com aliases de tabela dentro de subconsultas; assim um erro de digitação vira erro de execução, e não uma resposta errada.

4. Uma tabela derivada sem alias

SELECT COUNT(*) FROM (SELECT 1 FROM aircrafts) roda no DuckDB e no PostgreSQL 16 ou mais novo. O PostgreSQL 15 e anteriores, e alguns outros bancos, recusam com “subquery in FROM must have an alias”. Escrever AS t não custa nada e funciona em qualquer lugar.

5. ORDER BY dentro de uma subconsulta

Um ORDER BY dentro de uma tabela derivada não garante a ordem do resultado final. Ordene na consulta mais externa. Dentro de uma subconsulta, o ORDER BY só importa junto com LIMIT, como no exemplo do LATERAL.

Perguntas frequentes

O que é uma subconsulta no SQL?

Uma instrução SELECT aninhada dentro de outra instrução, entre parênteses. A consulta externa usa o resultado dela como um valor único, uma lista de valores ou uma tabela, dependendo de onde a subconsulta está.

Qual a diferença entre subconsulta correlacionada e não correlacionada?

Uma subconsulta não correlacionada não faz referência à consulta externa e pode ser executada sozinha; ela é avaliada uma vez. Uma subconsulta correlacionada faz referência a uma coluna da consulta externa (WHERE t2.fare_conditions = tf.fare_conditions), então o resultado depende da linha externa atual.

Subconsulta é mais lenta que JOIN?

Não por natureza. O PostgreSQL, o DuckDB e outros motores modernos reescrevem a maioria das subconsultas com IN e EXISTS como joins. Escreva a versão que expressa melhor a pergunta e só olhe o plano de execução se ela estiver de fato lenta.

Quando usar EXISTS em vez de IN?

Use EXISTS quando a condição depende da linha externa ou envolve várias colunas, e sempre prefira NOT EXISTS a NOT IN para “linhas sem correspondência”, porque o NOT IN quebra quando a lista contém um NULL.

Uma subconsulta pode devolver várias colunas?

No FROM, sim: uma tabela derivada pode ter quantas colunas quiser. No WHERE com IN, a subconsulta normalmente devolve uma coluna, embora o PostgreSQL também aceite comparações de linhas como (a, b) IN (SELECT x, y ...). No SELECT, a subconsulta precisa devolver um único valor.

Outros idiomas: English, Español, Tiếng Việt, 简体中文