kb.erickguedes.com
SQL: Domínio de Bancos Relacionais

Subqueries e CTEs

Aula 4 de 8

Subquery no WHERE

-- Clientes que gastaram mais que a média
SELECT c.nome, SUM(p.total) AS total_gasto
FROM clientes c
JOIN pedidos p ON p.cliente_id = c.id
GROUP BY c.id, c.nome
HAVING SUM(p.total) > (SELECT AVG(total) FROM pedidos);

-- EXISTS (mais performático que IN para subqueries grandes)
SELECT nome FROM clientes c
WHERE EXISTS (
    SELECT 1 FROM pedidos p
    WHERE p.cliente_id = c.id
    AND p.total > 1000
);

-- NOT EXISTS (clientes sem pedidos)
SELECT nome FROM clientes c
WHERE NOT EXISTS (
    SELECT 1 FROM pedidos p
    WHERE p.cliente_id = c.id
);

-- IN vs EXISTS
-- IN: SELECT nome FROM clientes WHERE id IN (SELECT cliente_id FROM pedidos)
-- EXISTS: melhor performance com subquery grande

Subquery no FROM

-- Subquery como tabela derivada (precisa de alias)
SELECT departamento, media_salarial
FROM (
    SELECT 
        departamento,
        AVG(salario) AS media_salarial,
        COUNT(*) AS total_func
    FROM funcionarios
    GROUP BY departamento
) AS dept_stats
WHERE total_func > 5
ORDER BY media_salarial DESC;

Subquery no SELECT

SELECT 
    c.nome,
    (SELECT COUNT(*) FROM pedidos p WHERE p.cliente_id = c.id) AS total_pedidos,
    (SELECT SUM(p.total) FROM pedidos p WHERE p.cliente_id = c.id) AS total_gasto,
    (SELECT AVG(total) FROM pedidos) AS media_geral,
    (SELECT SUM(p.total) FROM pedidos p WHERE p.cliente_id = c.id) 
        / NULLIF((SELECT AVG(total) FROM pedidos), 0) * 100 AS porcentagem_media
FROM clientes c;

CTE — Common Table Expression

-- WITH statement: organiza queries complexas
WITH vendas_por_mes AS (
    SELECT 
        DATE_TRUNC('month', data) AS mes,
        SUM(valor) AS receita,
        COUNT(*) AS transacoes
    FROM vendas
    WHERE data >= '2024-01-01'
    GROUP BY mes
),
variacao AS (
    SELECT 
        mes,
        receita,
        LAG(receita) OVER (ORDER BY mes) AS receita_anterior,
        (receita - LAG(receita) OVER (ORDER BY mes)) / 
            NULLIF(LAG(receita) OVER (ORDER BY mes), 0) * 100 AS crescimento
    FROM vendas_por_mes
)
SELECT * FROM variacao ORDER BY mes;

-- CTE recursiva (hierarquia)
WITH RECURSIVE subordinados AS (
    -- Base case
    SELECT id, nome, gerente_id, 1 AS nivel
    FROM funcionarios
    WHERE gerente_id IS NULL
    
    UNION ALL
    
    -- Recursive case
    SELECT f.id, f.nome, f.gerente_id, s.nivel + 1
    FROM funcionarios f
    JOIN subordinados s ON f.gerente_id = s.id
)
SELECT * FROM subordinados ORDER BY nivel, nome;

Lab: Análise Avançada

-- Top 10% clientes (NTILE)
WITH clientes_rank AS (
    SELECT 
        c.id,
        c.nome,
        SUM(p.total) AS total_gasto,
        NTILE(10) OVER (ORDER BY SUM(p.total) DESC) AS decil
    FROM clientes c
    LEFT JOIN pedidos p ON p.cliente_id = c.id
    GROUP BY c.id, c.nome
)
SELECT * FROM clientes_rank WHERE decil = 1;

-- Clientes que aumentaram gasto
WITH gasto_por_mes AS (
    SELECT 
        c.id,
        c.nome,
        DATE_TRUNC('month', p.data_pedido) AS mes,
        SUM(p.total) AS gasto
    FROM clientes c
    JOIN pedidos p ON p.cliente_id = c.id
    GROUP BY c.id, c.nome, mes
),
variacao AS (
    SELECT 
        *,
        LAG(gasto) OVER (PARTITION BY id ORDER BY mes) AS gasto_anterior,
        CASE 
            WHEN gasto > LAG(gasto) OVER (PARTITION BY id ORDER BY mes) 
            THEN 'aumentou' 
            ELSE 'diminuiu' 
        END AS tendencia
    FROM gasto_por_mes
)
SELECT nome, mes, gasto, gasto_anterior, tendencia
FROM variacao
WHERE tendencia = 'aumentou';

Subqueries: WHERE (filtro), FROM (tabela derivada), SELECT (coluna calculada). CTEs são mais legíveis que subqueries aninhadas. CTEs recursivas são ideais para hierarquias e grafos.