Subqueries: consultas dentro de consultas

1. Introdução às Subqueries

Uma subquery (ou subconsulta) é uma consulta SQL aninhada dentro de outra consulta. Ela permite que você utilize o resultado de uma consulta como entrada para outra, criando uma camada adicional de lógica e filtragem. O propósito principal é resolver problemas que exigem múltiplos passos de consulta em uma única instrução SQL.

A principal diferença entre subqueries e joins está na abordagem: enquanto joins combinam tabelas horizontalmente (colunas lado a lado), subqueries processam resultados verticalmente, onde o resultado de uma consulta alimenta outra. Subqueries são particularmente úteis quando você precisa comparar valores individuais com resultados agregados ou verificar condições de existência.

Vantagens das subqueries:
- Leitura mais intuitiva para certos problemas
- Isolamento lógico de partes da consulta
- Úteis para cálculos que dependem de agregações

Desvantagens:
- Podem ser menos eficientes que joins equivalentes
- Subqueries correlacionadas podem ter performance ruim em grandes volumes
- Legibilidade pode ser prejudicada com muitos aninhamentos

2. Subqueries Escalares

Subqueries escalares retornam exatamente um valor (uma linha e uma coluna). São frequentemente usadas em cláusulas SELECT ou WHERE para comparações diretas.

-- Exemplo: Listar funcionários com salário acima da média
SELECT nome, salario
FROM funcionarios
WHERE salario > (SELECT AVG(salario) FROM funcionarios);
-- Exemplo: Mostrar nome e diferença salarial em relação à média
SELECT nome, salario,
       salario - (SELECT AVG(salario) FROM funcionarios) AS diferenca_media
FROM funcionarios;

No primeiro exemplo, a subquery calcula a média salarial de todos os funcionários e a consulta externa filtra apenas aqueles com salário superior. No segundo, a subquery é usada dentro do SELECT para calcular a diferença individual.

3. Subqueries com Múltiplas Linhas

Quando uma subquery retorna uma coluna com várias linhas, utilizamos operadores especiais como IN, NOT IN, ANY e ALL.

-- IN: funcionários que trabalham em departamentos específicos
SELECT nome, departamento_id
FROM funcionarios
WHERE departamento_id IN (SELECT id FROM departamentos WHERE ativo = 1);
-- ANY: funcionários com salário maior que qualquer salário do departamento 5
SELECT nome, salario
FROM funcionarios
WHERE salario > ANY (SELECT salario FROM funcionarios WHERE departamento_id = 5);
-- ALL: funcionários com salário maior que todos do departamento 5
SELECT nome, salario
FROM funcionarios
WHERE salario > ALL (SELECT salario FROM funcionarios WHERE departamento_id = 5);

Cuidado com NULL em NOT IN: Se a subquery retornar qualquer valor NULL, o NOT IN retornará um conjunto vazio. Prefira NOT EXISTS nesses casos.

4. Subqueries na Cláusula FROM (Derived Tables)

Subqueries na cláusula FROM criam tabelas derivadas — resultados temporários que podem ser consultados como se fossem tabelas reais. É obrigatório usar um alias.

-- Exemplo: Média salarial por departamento, filtrando departamentos com mais de 3 funcionários
SELECT d.nome_departamento, dados.media_salarial, dados.total_funcionarios
FROM departamentos d
JOIN (
    SELECT departamento_id,
           AVG(salario) AS media_salarial,
           COUNT(*) AS total_funcionarios
    FROM funcionarios
    GROUP BY departamento_id
    HAVING COUNT(*) > 3
) AS dados ON d.id = dados.departamento_id;

Essa abordagem é excelente para criar agregações aninhadas ou aplicar múltiplos níveis de filtros que seriam complexos com subqueries simples.

5. Subqueries Correlacionadas

Subqueries correlacionadas fazem referência a colunas da consulta externa. Elas são executadas linha a linha pela consulta externa, o que pode impactar a performance.

-- Exemplo: Funcionários com salário acima da média do seu próprio departamento
SELECT f1.nome, f1.salario, f1.departamento_id
FROM funcionarios f1
WHERE f1.salario > (
    SELECT AVG(f2.salario)
    FROM funcionarios f2
    WHERE f2.departamento_id = f1.departamento_id
);

Aqui, para cada funcionário da consulta externa, a subquery calcula a média salarial do departamento correspondente. Isso significa que a subquery é executada repetidamente — uma vez para cada linha da tabela externa.

6. EXISTS e NOT EXISTS

EXISTS verifica se uma subquery retorna alguma linha, enquanto NOT EXISTS verifica se retorna nenhuma. A principal diferença para IN é que EXISTS é mais eficiente com subqueries correlacionadas e lida melhor com valores NULL.

-- Exemplo: Clientes que realizaram pelo menos um pedido
SELECT c.nome, c.email
FROM clientes c
WHERE EXISTS (
    SELECT 1
    FROM pedidos p
    WHERE p.cliente_id = c.id
);
-- Exemplo: Clientes que nunca fizeram pedidos
SELECT c.nome, c.email
FROM clientes c
WHERE NOT EXISTS (
    SELECT 1
    FROM pedidos p
    WHERE p.cliente_id = c.id
);

O SELECT 1 dentro do EXISTS é uma convenção — como o operador apenas verifica existência, o conteúdo selecionado é irrelevante. Isso torna a execução mais rápida que IN, que precisa materializar todos os resultados da subquery.

7. Subqueries na Cláusula HAVING

A cláusula HAVING filtra grupos após a agregação. Subqueries aqui permitem comparar métricas de grupos com valores calculados externamente.

-- Exemplo: Departamentos com salário médio acima da média geral da empresa
SELECT departamento_id, AVG(salario) AS media_salarial
FROM funcionarios
GROUP BY departamento_id
HAVING AVG(salario) > (
    SELECT AVG(salario) FROM funcionarios
);
-- Exemplo: Departamentos cujo total de salários é maior que 10% do total geral
SELECT departamento_id, SUM(salario) AS total_salarios
FROM funcionarios
GROUP BY departamento_id
HAVING SUM(salario) > (
    SELECT SUM(salario) * 0.1 FROM funcionarios
);

8. Boas Práticas e Performance

Quando evitar subqueries:
- Se a subquery pode ser reescrita como um JOIN com mesma semântica, prefira o JOIN — geralmente mais eficiente
- Subqueries correlacionadas em tabelas grandes podem ser extremamente lentas; considere Common Table Expressions (CTEs) ou tabelas temporárias
- Evite aninhamentos profundos (mais de 2-3 níveis) — prejudicam legibilidade e manutenção

Impacto no plano de execução:
- Subqueries não correlacionadas são executadas uma vez e reutilizadas
- Subqueries correlacionadas executam para cada linha externa — O(n * m) no pior caso
- Use EXPLAIN para analisar o plano de execução e identificar gargalos

Dicas de otimização:
- Prefira EXISTS a IN quando a subquery for grande ou correlacionada
- Use índices nas colunas envolvidas nas condições da subquery
- Considere transformar subqueries complexas em CTEs para melhor legibilidade e, em alguns bancos, melhor planos de execução

-- Exemplo: Refatorando subquery correlacionada para CTE (mais legível e potencialmente mais eficiente)
WITH medias_departamento AS (
    SELECT departamento_id, AVG(salario) AS media
    FROM funcionarios
    GROUP BY departamento_id
)
SELECT f.nome, f.salario, f.departamento_id
FROM funcionarios f
JOIN medias_departamento md ON f.departamento_id = md.departamento_id
WHERE f.salario > md.media;

Referências