14

Subconsultas & CTEs PostgreSQL

Plano de Aula & Guia Prático de Aprendizagem

Elevando Consultas Simples para Arquitetura de Dados Complexa

SGBD: PostgreSQL / Supabase | 32 Alunos

Contexto Pedagógico (A Pergunta de Duas Etapas)

Imagine que a diretoria solicite: "Listar todos os produtos com preço maior que a média da loja". O erro mais comum dos estudantes é tentar rodar WHERE preco > AVG(preco), o que gera erro imediato porque funções de agregação não podem rodar diretamente na cláusula WHERE sem uma consulta prévia.

❌ Abordagem Errada (Erro de Sintaxe) SELECT * FROM produtos WHERE preco > AVG(preco);
✔ Abordagem Correta (Subconsulta Escalar) SELECT * FROM produtos WHERE preco > (SELECT AVG(preco) FROM produtos);
PASSO 00

Script de Preparação do Banco (Criação de Tabelas & Dados)

Execute o script abaixo no SQL Editor do Supabase / PostgreSQL para criar as tabelas relacionais (clientes, produtos, pedidos, itens_pedido) e popular a base de testes.

-- =================================================================
-- PASSO 00: SCHEMA & SEED DATA (POSTGRESQL / SUPABASE)
-- =================================================================

-- 1. Limpeza prévia de tabelas
DROP TABLE IF EXISTS itens_pedido CASCADE;
DROP TABLE IF EXISTS pedidos CASCADE;
DROP TABLE IF EXISTS produtos CASCADE;
DROP TABLE IF EXISTS clientes CASCADE;

-- 2. Criação das Tabelas (DDL)
CREATE TABLE clientes (
    id_cliente SERIAL PRIMARY KEY,
    nome VARCHAR(100) NOT NULL,
    email VARCHAR(100) UNIQUE NOT NULL,
    saldo_pontos INT DEFAULT 0
);

CREATE TABLE produtos (
    id_produto SERIAL PRIMARY KEY,
    nome_produto VARCHAR(100) NOT NULL,
    categoria VARCHAR(50) NOT NULL,
    preco DECIMAL(10, 2) NOT NULL
);

CREATE TABLE pedidos (
    id_pedido SERIAL PRIMARY KEY,
    id_cliente INT REFERENCES clientes(id_cliente),
    data_pedido DATE NOT NULL,
    valor_total DECIMAL(10, 2) NOT NULL,
    status VARCHAR(20) DEFAULT 'Concluído'
);

CREATE TABLE itens_pedido (
    id_item SERIAL PRIMARY KEY,
    id_pedido INT REFERENCES pedidos(id_pedido),
    id_produto INT REFERENCES produtos(id_produto),
    quantidade INT NOT NULL,
    preco_unitario DECIMAL(10, 2) NOT NULL
);

-- 3. Inserção dos Dados de Teste (DML)
INSERT INTO clientes (nome, email, saldo_pontos) VALUES
('Ana Silva', 'ana.silva@email.com', 6500),
('Carlos Oliveira', 'carlos.o@email.com', 1200),
('Mariana Costa', 'mariana.costa@email.com', 5800),
('Beatriz Lima', 'beatriz.l@email.com', 450),
('Lucas Mendes', 'lucas.mendes@email.com', 0);

INSERT INTO produtos (nome_produto, categoria, preco) VALUES
('Notebook Gamer i9', 'Eletrônicos', 8500.00),
('Monitor Ultrawide 34"', 'Eletrônicos', 3200.00),
('Teclado Mecânico RGB', 'Acessórios', 350.00),
('Mouse Sem Fio Ergonomico', 'Acessórios', 180.00),
('Cadeira Ergonômica', 'Móveis', 1200.00),
('Suporte para Monitor', 'Acessórios', 90.00),
('Fone Noise Cancelling', 'Eletrônicos', 1400.00);

INSERT INTO pedidos (id_cliente, data_pedido, valor_total, status) VALUES
(1, '2026-09-05', 8850.00, 'Concluído'),
(1, '2026-09-15', 350.00, 'Concluído'),
(2, '2026-08-10', 3200.00, 'Concluído'),
(3, '2026-09-20', 1200.00, 'Concluído'),
(3, '2026-09-22', 1400.00, 'Pendente');

INSERT INTO itens_pedido (id_pedido, id_produto, quantidade, preco_unitario) VALUES
(1, 1, 1, 8500.00),
(1, 3, 1, 350.00),
(2, 3, 1, 350.00),
(3, 2, 1, 3200.00),
(4, 5, 1, 1200.00),
(5, 7, 1, 1400.00);
PASSO 01

Subconsulta Escalar no WHERE (Retorno de Valor Único)

Subconsultas escalares retornam exatamente 1 linha e 1 coluna (um único valor numerico ou de texto), permitindo a utilização de operadores de comparação padrão (>, <, =, >=, <=).

-- Exemplo 1: Produtos com preço acima da média geral da loja
SELECT 
    id_produto, 
    nome_produto, 
    preco
FROM produtos
WHERE preco > (
    SELECT AVG(preco) 
    FROM produtos
)
ORDER BY preco DESC;
Resultado Esperado no Supabase / PostgreSQL
id_produto nome_produto preco
105Notebook Gamer i9R$ 8.500,00
102Monitor Ultrawide 34"R$ 3.200,00
PASSO 02

Filtrando Conjuntos com IN e NOT IN

Quando a subconsulta gera uma lista de múltiplos valores (uma coluna com N linhas), utilizamos os operadores IN e NOT IN.

-- Exemplo 2.1: Listar clientes que realizaram pedidos no último mês
SELECT id_cliente, nome, email
FROM clientes
WHERE id_cliente IN (
    SELECT DISTINCT id_cliente 
    FROM pedidos 
    WHERE data_pedido >= '2026-09-01'
);

-- Exemplo 2.2: Listar produtos que NUNCA foram vendidos
SELECT id_produto, nome_produto
FROM produtos
WHERE id_produto NOT IN (
    SELECT DISTINCT id_produto 
    FROM itens_pedido 
    WHERE id_produto IS NOT NULL
);
PASSO 03

Alta Performance de Busca: EXISTS e NOT EXISTS

O operador EXISTS realiza uma subconsulta correlacionada. Ele aplica o efeito Short-Circuit (Curto-circuito): assim que encontra o primeiro registro correspondente, ele interrompe a busca naquela linha e retorna TRUE, sendo drasticamente mais veloz em grandes bases de dados que o IN.

-- Exemplo 3: Clientes ativos que possuem pelo menos 1 pedido registrado (Usando EXISTS)
SELECT 
    c.id_cliente, 
    c.nome
FROM clientes c
WHERE EXISTS (
    SELECT 1 
    FROM pedidos p 
    WHERE p.id_cliente = c.id_cliente
);
PASSO 04

Operadores Multivalorados: ANY e ALL

> ALL (...) exige que o valor seja superior a todos os itens retornados pela lista (ou seja, maior que o valor máximo). > ANY (...) requer ser maior que pelo menos um dos valores (maior que o mínimo).

-- Exemplo 4.1 (> ALL): Produtos mais caros do que TODOS os produtos da categoria 'Acessórios'
SELECT nome_produto, preco
FROM produtos
WHERE preco > ALL (
    SELECT preco 
    FROM produtos 
    WHERE categoria = 'Acessórios'
);

-- Exemplo 4.2 (> ANY): Produtos mais caros do que QUALQUER produto da categoria 'Acessórios'
SELECT nome_produto, preco
FROM produtos
WHERE preco > ANY (
    SELECT preco 
    FROM produtos 
    WHERE categoria = 'Acessórios'
);
PASSO 05

Subconsultas na Cláusula SELECT (Campos Calculados)

Podemos inserir um SELECT interno como se fosse uma nova coluna virtual. Atenção: Essa subconsulta executa linha a linha.

-- Exemplo 5: Exibir clientes e uma coluna com o valor total acumulado em compras
SELECT 
    c.id_cliente,
    c.nome,
    (
        SELECT COALESCE(SUM(p.valor_total), 0)
        FROM pedidos p 
        WHERE p.id_cliente = c.id_cliente
    ) AS total_gasto_historico
FROM clientes c
ORDER BY total_gasto_historico DESC;
PASSO 06

Refatoração para Arquitetura Limpa com CTEs (Cláusula WITH)

O Padrão da Indústria Moderno: As CTEs (Common Table Expressions) criam tabelas temporárias nomeadas em memória, eliminando a bagunça do "código macarrão" e permitindo leitura sequencial de cima para baixo.
-- Exemplo 6: Refatorando consultas complexas usando CTEs (WITH)
WITH MediaPrecoCategorias AS (
    SELECT 
        categoria, 
        AVG(preco) AS media_categoria
    FROM produtos
    GROUP BY categoria
),
ClientesVIP AS (
    SELECT id_cliente, nome 
    FROM clientes 
    WHERE saldo_pontos > 5000
)
-- Consulta Final unificando as CTEs temporárias:
SELECT 
    p.nome_produto, 
    p.categoria, 
    p.preco, 
    ROUND(mpc.media_categoria, 2) AS media_da_categoria
FROM produtos p
INNER JOIN MediaPrecoCategorias mpc ON p.categoria = mpc.categoria
WHERE p.preco > mpc.media_categoria
ORDER BY p.categoria, p.preco DESC;