📚 SQL do Zero - Documentação Completa

Guia definitivo de SQL para entrevistas e referência prática

🔤 ALIASES (Apelidos de Tabelas)

O que são?

Aliases são apelidos curtos dados às tabelas para facilitar a escrita de queries.

FROM clientes c
     ^^^^^^^^^ ^
     tabela    alias

Por quê usar?

  • ✅ Escrever menos (c.nome ao invés de clientes.nome)
  • ✅ Obrigatório em JOINs quando há colunas com mesmo nome
  • ✅ Deixa query mais legível
❌ Sem alias (ruim)
SELECT clientes.nome, pedidos.data_pedido
FROM clientes
JOIN pedidos ON clientes.id = pedidos.cliente_id;
✅ Com alias (bom)
SELECT c.nome, p.data_pedido
FROM clientes c
JOIN pedidos p ON c.id = p.cliente_id;
📌 Aliases Comuns: clientes cpedidos pprodutos pritens_pedido ip

🔗 NOTAÇÃO tabela.coluna

O que é?

SELECT c.nome, p.status
       ^-----  ^-------
       tabela  tabela
       coluna  coluna

Quando usar?

✅ SEMPRE em JOINs Evita ambiguidade quando múltiplas tabelas têm colunas com mesmo nome
Problema: Ambiguidade
-- ❌ ERRADO (ambíguo - qual id?):
SELECT id, nome
FROM clientes c
JOIN pedidos p ON c.id = p.cliente_id;
-- ERRO: column "id" is ambiguous

-- ✅ CERTO (específico):
SELECT c.id, c.nome
FROM clientes c
JOIN pedidos p ON c.id = p.cliente_id;
⚠️ Boa Prática: Mesmo quando não obrigatório, use sempre tabela.coluna para clareza

🎯 SELECT - Seleção de Colunas

Sintaxe Básica

SELECT coluna1, coluna2, coluna3
FROM tabela;

Variações

1. SELECT * (tudo)

SELECT * FROM clientes;
-- ⚠️ Ruim em produção (traz dados desnecessários)
-- ✅ Bom para exploração inicial

2. SELECT específico

SELECT nome, email FROM clientes;
-- ✅ Melhor performance
-- ✅ Mais claro

3. SELECT com alias de coluna

SELECT 
    nome AS nome_cliente,
    email AS email_contato
FROM clientes;

4. SELECT com cálculos

SELECT 
    nome,
    preco,
    preco * 0.9 AS preco_com_desconto
FROM produtos;

5. SELECT com funções agregadas

SELECT 
    COUNT(*) AS total_clientes,
    AVG(valor_total) AS ticket_medio,
    SUM(valor_total) AS receita_total
FROM pedidos;

🔍 WHERE - Filtros

Sintaxe

SELECT colunas
FROM tabela
WHERE condição;

Operadores

Operador Descrição Exemplo
= Igualdade WHERE cidade = 'São Paulo'
!= ou <> Diferente WHERE status != 'Cancelado'
>, <, >=, <= Comparação WHERE preco > 100
BETWEEN Entre valores WHERE preco BETWEEN 100 AND 500
IN Está na lista WHERE categoria IN ('Eletrônicos', 'Roupas')
LIKE Padrão de texto WHERE nome LIKE 'João%'
IS NULL É nulo WHERE email IS NULL
AND / OR Combinar condições WHERE cidade = 'SP' AND status = 'Pago'

LIKE - Padrões

% = qualquer sequência de caracteres
_ = exatamente 1 caractere
'João%'    -- começa com João
'%@gmail.com' -- termina com @gmail.com
'%Silva%'  -- contém Silva
'_oão'     -- J + oão (4 chars)

🔗 JOIN - Junção de Tabelas

Por quê JOIN existe?

Dados estão espalhados em várias tabelas (normalização). JOIN permite combinar essas informações.

INNER JOIN (mais comum)

SELECT c.nome, p.data_pedido, p.valor_total
FROM clientes c
INNER JOIN pedidos p ON c.id = p.cliente_id;
✅ O que faz: Retorna SÓ linhas que têm match nas DUAS tabelas

LEFT JOIN

SELECT c.nome, p.data_pedido
FROM clientes c
LEFT JOIN pedidos p ON c.id = p.cliente_id;
📘 O que faz: Retorna TODOS da tabela da esquerda + matches da direita (NULL se não houver match)

Diagrama Visual

INNER JOIN: Só o que combina ⚡
LEFT JOIN: Todos da esquerda + matches da direita 👈
RIGHT JOIN: Todos da direita + matches da esquerda 👉

JOIN Múltiplos (3+ tabelas)

SELECT c.nome, p.data_pedido, pr.nome AS produto
FROM clientes c
INNER JOIN pedidos p ON c.id = p.cliente_id
INNER JOIN itens_pedido ip ON p.id = ip.pedido_id
INNER JOIN produtos pr ON ip.produto_id = pr.id;
clientes pedidos itens_pedido produtos
⚠️ Regra Geral: JOIN liga Primary Key (PK) com Foreign Key (FK)

📊 GROUP BY - Agrupamento

O que faz?

Agrupa linhas com valores iguais e permite calcular totais, médias, contagens, etc.

SELECT 
    cidade,
    COUNT(*) AS total_clientes
FROM clientes
GROUP BY cidade;

🎯 Regra de Ouro

❗ IMPORTANTE: Se usa função agregada (COUNT, SUM, AVG), PRECISA de GROUP BY
❌ ERRADO
SELECT cidade, COUNT(*)
FROM clientes;
-- ERRO: falta GROUP BY
✅ CERTO
SELECT cidade, COUNT(*)
FROM clientes
GROUP BY cidade;

O que pode ir no SELECT com GROUP BY?

  • Colunas do GROUP BY
  • Funções agregadas (COUNT, SUM, AVG, MIN, MAX)
  • Colunas normais (não agrupadas)

GROUP BY múltiplas colunas

SELECT 
    cidade,
    status,
    COUNT(*) AS total
FROM pedidos p
JOIN clientes c ON p.cliente_id = c.id
GROUP BY cidade, status;

🔢 FUNÇÕES AGREGADAS

Função O que faz Exemplo
COUNT(*) Conta todas as linhas COUNT(*) AS total
COUNT(coluna) Conta linhas onde coluna NÃO é NULL COUNT(email)
COUNT(DISTINCT) Conta valores únicos COUNT(DISTINCT cidade)
SUM() Soma valores SUM(valor_total)
AVG() Média aritmética AVG(preco)
MIN() Valor mínimo MIN(preco)
MAX() Valor máximo MAX(preco)

Exemplo Completo

SELECT 
    categoria,
    COUNT(*) AS total_produtos,
    AVG(preco) AS preco_medio,
    MIN(preco) AS mais_barato,
    MAX(preco) AS mais_caro,
    SUM(estoque) AS estoque_total
FROM produtos
GROUP BY categoria;

🎯 HAVING - Filtro em Agregações

WHERE vs HAVING

WHERE
Filtra ANTES de agrupar
(linhas individuais)
HAVING
Filtra DEPOIS de agrupar
(grupos resultantes)

Exemplo

-- Cidades com mais de 1000 clientes:
SELECT 
    cidade,
    COUNT(*) AS total
FROM clientes
GROUP BY cidade
HAVING COUNT(*) > 1000;
❌ ERRO COMUM: Não pode usar WHERE com funções agregadas
❌ ERRADO
SELECT cidade, COUNT(*)
FROM clientes
WHERE COUNT(*) > 1000
GROUP BY cidade;
-- ERRO!
✅ CERTO
SELECT cidade, COUNT(*)
FROM clientes
GROUP BY cidade
HAVING COUNT(*) > 1000;

WHERE + HAVING juntos

SELECT 
    cidade,
    COUNT(*) AS total
FROM clientes
WHERE data_cadastro >= '2024-01-01'  -- filtra linhas ANTES
GROUP BY cidade
HAVING COUNT(*) > 100;  -- filtra grupos DEPOIS
WHERE filtra GROUP BY agrupa HAVING filtra

📋 ORDER BY - Ordenação

Sintaxe

SELECT colunas
FROM tabela
ORDER BY coluna [ASC|DESC];
ASC = Crescente (padrão - A→Z, 1→9)
DESC = Decrescente (Z→A, 9→1)

Exemplos

-- Ordem alfabética (A→Z):
SELECT nome FROM clientes
ORDER BY nome ASC;

-- Ordem inversa (Z→A):
SELECT nome FROM clientes
ORDER BY nome DESC;

-- Múltiplas colunas:
SELECT nome, cidade, preco
FROM produtos
ORDER BY cidade ASC, preco DESC;
-- Ordena por cidade, depois por preço dentro de cada cidade

ORDER BY com agregação

SELECT 
    cidade,
    COUNT(*) AS total
FROM clientes
GROUP BY cidade
ORDER BY total DESC;  -- ordena pelo COUNT

🔝 LIMIT - Limitar Resultados

Sintaxe

SELECT colunas
FROM tabela
LIMIT número;

Usos Comuns

-- Top 10:
SELECT nome, preco
FROM produtos
ORDER BY preco DESC
LIMIT 10;

-- Paginação (primeira página):
SELECT * FROM clientes
LIMIT 20 OFFSET 0;  -- primeiros 20

-- Segunda página:
SELECT * FROM clientes
LIMIT 20 OFFSET 20;  -- próximos 20
⚠️ SEMPRE use ORDER BY com LIMIT
Sem ORDER BY, a ordem é imprevisível

🎨 DISTINCT - Valores Únicos

O que faz?

Remove duplicatas do resultado.

Sem DISTINCT
SELECT cidade
FROM clientes;
-- São Paulo
-- São Paulo
-- Rio
-- São Paulo
Com DISTINCT
SELECT DISTINCT cidade
FROM clientes;
-- São Paulo
-- Rio

DISTINCT em múltiplas colunas

SELECT DISTINCT cidade, status
FROM pedidos p
JOIN clientes c ON p.cliente_id = c.id;
-- Retorna combinações únicas de (cidade, status)

📅 FUNÇÕES DE DATA

DATE_TRUNC - Truncar data

-- Agrupa por mês (ignora dia/hora):
SELECT 
    DATE_TRUNC('month', data_pedido) AS mes,
    COUNT(*) AS total
FROM pedidos
GROUP BY DATE_TRUNC('month', data_pedido);
Opções: 'year', 'month', 'week', 'day', 'hour'

DATE / TIMESTAMP

-- Data atual:
SELECT CURRENT_DATE;  -- 2024-01-15
SELECT CURRENT_TIMESTAMP;  -- 2024-01-15 14:30:22
SELECT NOW();  -- mesmo que CURRENT_TIMESTAMP

-- Converter timestamp pra data:
SELECT DATE(data_pedido) FROM pedidos;

Intervalos

-- 30 dias atrás:
SELECT * FROM pedidos
WHERE data_pedido >= CURRENT_DATE - INTERVAL '30 days';

-- 6 meses atrás:
WHERE data_pedido >= CURRENT_DATE - INTERVAL '6 months';

-- 1 ano atrás:
WHERE data_pedido >= CURRENT_DATE - INTERVAL '1 year';

🔄 SUBQUERIES - Queries Dentro de Queries

1. No WHERE (filtrar)

-- Clientes que fizeram pedido:
SELECT nome
FROM clientes
WHERE id IN (
    SELECT cliente_id 
    FROM pedidos 
    WHERE status = 'Pago'
);

2. No FROM (tabela temporária)

SELECT 
    categoria,
    AVG(total_vendas) AS media_vendas
FROM (
    SELECT 
        pr.categoria,
        SUM(ip.quantidade) AS total_vendas
    FROM produtos pr
    JOIN itens_pedido ip ON pr.id = ip.produto_id
    GROUP BY pr.categoria
) AS subquery
GROUP BY categoria;

3. No SELECT (coluna calculada)

SELECT 
    nome,
    (SELECT COUNT(*) 
     FROM pedidos p 
     WHERE p.cliente_id = c.id) AS total_pedidos
FROM clientes c;
⚠️ Performance: Subqueries podem ser lentas. Prefira JOINs quando possível.

🧩 CASE - Condicional (IF no SQL)

Sintaxe

CASE 
    WHEN condição1 THEN resultado1
    WHEN condição2 THEN resultado2
    ELSE resultado_padrao
END

Exemplo Básico

SELECT 
    nome,
    preco,
    CASE 
        WHEN preco < 100 THEN 'Barato'
        WHEN preco BETWEEN 100 AND 500 THEN 'Médio'
        ELSE 'Caro'
    END AS categoria_preco
FROM produtos;

CASE com Agregação

SELECT 
    COUNT(CASE WHEN status = 'Pago' THEN 1 END) AS pagos,
    COUNT(CASE WHEN status = 'Pendente' THEN 1 END) AS pendentes,
    COUNT(CASE WHEN status = 'Cancelado' THEN 1 END) AS cancelados
FROM pedidos;

⚡ ORDEM DE EXECUÇÃO SQL

🎯 MUITO IMPORTANTE PARA ENTREVISTAS!
Entender a ordem de execução explica MUITOS erros comuns.
1. FROM - Pega tabelas
2. JOIN - Junta tabelas
3. WHERE - Filtra linhas
4. GROUP BY - Agrupa
5. HAVING - Filtra grupos
6. SELECT - Escolhe colunas
7. DISTINCT - Remove duplicatas
8. ORDER BY - Ordena
9. LIMIT - Limita quantidade

Estrutura Mental

SELECT [o que quero ver]
FROM [de onde]
JOIN [juntar com outra tabela] ON [condição]
WHERE [filtro antes de agrupar]
GROUP BY [agrupar por]
HAVING [filtro depois de agrupar]
ORDER BY [ordenar]
LIMIT [quantos]

💪 EXERCÍCIOS PRÁTICOS

✅ Setup Completo
Use o script de criação de banco de treino fornecido anteriormente.

Nível 1: Básico

📝 Exercício 1: Top 10 Clientes

Objetivo: Mostrar os 10 clientes que mais gastaram (status = 'Pago')

💡 Ver Solução
SELECT 
    c.nome,
    c.email,
    COUNT(p.id) AS total_pedidos,
    SUM(p.valor_total) AS total_gasto
FROM clientes c
JOIN pedidos p ON c.id = p.cliente_id
WHERE p.status = 'Pago'
GROUP BY c.id, c.nome, c.email
ORDER BY total_gasto DESC
LIMIT 10;
📝 Exercício 2: Produtos Mais Vendidos

Objetivo: Listar produtos mais vendidos por categoria

💡 Ver Solução
SELECT 
    pr.categoria,
    pr.nome,
    SUM(ip.quantidade) AS total_vendido,
    SUM(ip.quantidade * ip.preco_unitario) AS receita
FROM produtos pr
JOIN itens_pedido ip ON pr.id = ip.produto_id
JOIN pedidos p ON ip.pedido_id = p.id
WHERE p.status = 'Pago'
GROUP BY pr.categoria, pr.id, pr.nome
ORDER BY pr.categoria, total_vendido DESC;
📝 Exercício 3: Vendas por Mês

Objetivo: Vendas dos últimos 6 meses com ticket médio

💡 Ver Solução
SELECT 
    DATE_TRUNC('month', data_pedido) AS mes,
    COUNT(*) AS total_pedidos,
    SUM(valor_total) AS receita,
    AVG(valor_total) AS ticket_medio
FROM pedidos
WHERE data_pedido >= CURRENT_DATE - INTERVAL '6 months'
    AND status = 'Pago'
GROUP BY DATE_TRUNC('month', data_pedido)
ORDER BY mes DESC;

Nível 2: Otimização

📝 Exercício 5: Reescrever Queries Ineficientes

Objetivo: Melhorar performance de queries comuns

❌ Query Ruim 1:
SELECT nome 
FROM clientes 
WHERE id IN (
    SELECT cliente_id 
    FROM pedidos 
    WHERE status = 'Pago'
);
💡 Ver Solução Otimizada
✅ Query Boa:
-- JOIN é mais eficiente que subquery IN
SELECT DISTINCT c.nome 
FROM clientes c
JOIN pedidos p ON c.id = p.cliente_id
WHERE p.status = 'Pago';

Nível 3: Cenários Avançados

📝 Exercício 6: Window Functions

Objetivo: Ranking de clientes por cidade

💡 Ver Solução
SELECT 
    nome,
    cidade,
    total_pedidos,
    RANK() OVER (
        PARTITION BY cidade 
        ORDER BY total_pedidos DESC
    ) AS ranking_cidade
FROM (
    SELECT 
        c.nome,
        c.cidade,
        COUNT(p.id) AS total_pedidos
    FROM clientes c
    JOIN pedidos p ON c.id = p.cliente_id
    WHERE p.status = 'Pago'
    GROUP BY c.id, c.nome, c.cidade
) sub
ORDER BY cidade, ranking_cidade;
🎯 Rotina de Treino Recomendada:
Semana 1: Exercícios 1-3 (básico) - 45 min/dia
Semana 2: Exercícios 4-5 (otimização) - 45 min/dia
Semana 3: Exercício 6 (avançado) + revisar tudo - 30 min/dia

🐘 COMANDOS POSTGRES ÚTEIS

Análise de Performance

-- Ver plano de execução:
EXPLAIN SELECT * FROM clientes;

-- Ver plano + tempo real de execução:
EXPLAIN ANALYZE SELECT * FROM clientes;

-- Ver queries rodando agora:
SELECT pid, usename, state, query
FROM pg_stat_activity
WHERE state = 'active';

-- Ver tamanho de tabelas:
SELECT 
    tablename,
    pg_size_pretty(pg_total_relation_size(tablename::text)) AS size
FROM pg_tables
WHERE schemaname = 'public'
ORDER BY pg_total_relation_size(tablename::text) DESC;

Gerenciamento de Índices

-- Listar todos os índices:
SELECT 
    tablename,
    indexname,
    indexdef
FROM pg_indexes
WHERE schemaname = 'public';

-- Criar índice:
CREATE INDEX idx_nome ON tabela(coluna);

-- Criar índice composto:
CREATE INDEX idx_nome ON tabela(coluna1, coluna2);

-- Remover índice:
DROP INDEX idx_nome;

-- Reindexar (quando índice fragmentado):
REINDEX TABLE tabela;

Backup & Recovery

-- Backup de um banco (terminal):
docker exec [container] pg_dump -U postgres dbname > backup.sql

-- Restaurar backup:
docker exec -i [container] psql -U postgres dbname < backup.sql

-- Backup de todos os bancos:
docker exec [container] pg_dumpall -U postgres > backup_all.sql

Estatísticas e Manutenção

-- Atualizar estatísticas (importante para performance):
ANALYZE tabela;

-- Limpar espaço morto:
VACUUM tabela;

-- VACUUM completo (trava tabela):
VACUUM FULL tabela;

-- VACUUM + ANALYZE juntos:
VACUUM ANALYZE tabela;

❓ PERGUNTAS COMUNS DE ENTREVISTA

1. Diferença entre INNER JOIN e LEFT JOIN?
✅ Resposta:
INNER JOIN: Retorna SOMENTE linhas que têm match nas DUAS tabelas.
LEFT JOIN: Retorna TODAS as linhas da tabela da esquerda + matches da direita. Se não houver match, preenche com NULL.
2. Quando criar um índice?
✅ Resposta:
Criar índices em colunas usadas em:
• WHERE (filtros)
• JOIN (chaves estrangeiras)
• ORDER BY (ordenação)

NÃO criar em:
• Tabelas pequenas (< 1000 linhas)
• Colunas com poucos valores distintos (ex: boolean)
• Tabelas com muitos INSERTs (índice desacelera writes)
3. O que é VACUUM no Postgres?
✅ Resposta:
VACUUM limpa espaço de linhas deletadas/atualizadas. PostgreSQL não remove linhas fisicamente no DELETE/UPDATE, deixa "dead tuples". VACUUM recupera esse espaço e atualiza estatísticas para o query planner.
4. O que significa ACID?
✅ Resposta:
Atomicity (Atomicidade): Transação é tudo ou nada
Consistency (Consistência): Dados sempre válidos
Isolation (Isolamento): Transações não interferem entre si
Durability (Durabilidade): Commit = dados permanentes
5. Como você resolve uma query lenta em produção?
✅ Resposta (método estruturado):
1. Identificar: Uso EXPLAIN ANALYZE pra ver o plano
2. Gargalos: Procuro Seq Scan, Sort, Hash Join caros
3. Índices: Verifico índices faltantes (DMVs/pg_stat)
4. Implementar: Crio índices apropriados
5. Validar: Testo em homolog, comparo tempos
6. Monitorar: Aplico em prod e monitoro impacto
6. Diferença entre WHERE e HAVING?
✅ Resposta:
WHERE: Filtra linhas ANTES de agrupar (linhas individuais)
HAVING: Filtra DEPOIS de agrupar (grupos resultantes)

WHERE não pode usar funções agregadas. HAVING pode.
7. O que é normalização?
✅ Resposta:
Processo de organizar dados para minimizar redundância e dependências. As 3 formas normais principais:
1NF: Valores atômicos (sem arrays/listas)
2NF: Sem dependências parciais da chave
3NF: Sem dependências transitivas

Na prática, normalizo até 3NF. Às vezes denormalizo propositalmente para performance (ex: tabelas agregadas para relatórios).

📖 GLOSSÁRIO RÁPIDO

Termo Significado
PK (Primary Key) Chave primária - identifica linha única
FK (Foreign Key) Chave estrangeira - referencia PK de outra tabela
Schema Estrutura do banco (tabelas, colunas, tipos)
Index Estrutura que acelera buscas (como índice de livro)
Query Consulta SQL (SELECT, INSERT, UPDATE, DELETE)
Transaction Conjunto de operações (tudo ou nada)
Commit Confirma transação (torna permanente)
Rollback Desfaz transação (volta ao estado anterior)
Deadlock 2 transações travadas esperando uma pela outra
Lock Trava em linha/tabela durante modificação
Seq Scan Lê tabela inteira linha por linha (lento)
Index Scan Usa índice para buscar (rápido)
Aggregation Operação que resume dados (COUNT, SUM, AVG)
Subquery Query dentro de outra query
CTE Common Table Expression (WITH clause)

⚡ COMANDOS RÁPIDOS (CHEAT SHEET)

🔍 Consultas Básicas

SELECT * FROM tabela;
SELECT col1, col2 FROM tabela;
SELECT DISTINCT coluna FROM tabela;
SELECT * FROM tabela WHERE col = 'valor';
SELECT * FROM tabela ORDER BY col DESC;
SELECT * FROM tabela LIMIT 10;

🔗 JOINs

SELECT * FROM t1
INNER JOIN t2 ON t1.id = t2.id;

SELECT * FROM t1
LEFT JOIN t2 ON t1.id = t2.id;

SELECT * FROM t1
RIGHT JOIN t2 ON t1.id = t2.id;

📊 Agregações

SELECT COUNT(*) FROM tabela;
SELECT SUM(coluna) FROM tabela;
SELECT AVG(coluna) FROM tabela;
SELECT MIN(coluna), MAX(coluna) FROM tabela;
SELECT col, COUNT(*)
FROM tabela GROUP BY col;

🔍 Filtros

WHERE col = 'valor'
WHERE col IN ('a', 'b', 'c')
WHERE col BETWEEN 10 AND 50
WHERE col LIKE '%texto%'
WHERE col IS NULL
WHERE col1 = 'a' AND col2 > 10

📅 Datas

CURRENT_DATE
CURRENT_TIMESTAMP
NOW()
DATE_TRUNC('month', coluna)
WHERE data >= CURRENT_DATE - INTERVAL '30 days'

🔧 Índices

CREATE INDEX idx_nome ON tabela(coluna);
CREATE INDEX idx_nome ON tabela(col1, col2);
DROP INDEX idx_nome;

🎯 Você está pronto!

Esta documentação cobre 90% do que cai em entrevistas.
Pratique os exercícios, revise os conceitos e você vai arrasar! 🚀

Dica final: Salve esta página (Ctrl+S) e revise antes das entrevistas.

📚 Criado para treino de entrevistas técnicas em SQL
💪 Pratique, erre, aprenda, repita
🚀 Boa sorte nas entrevistas!

⚡ Dica: Use EXPLAIN ANALYZE antes de cada query para ver o plano de execução!
📝 Exercício 4: Otimizar Query Lenta

Objetivo: Identificar e resolver gargalos de performance

-- 1. Rode essa query (vai demorar):
EXPLAIN ANALYZE
SELECT 
    c.nome,
    COUNT(p.id) AS total_pedidos
FROM clientes c
LEFT JOIN pedidos p ON c.id = p.cliente_id
WHERE c.cidade = 'São Paulo'
GROUP BY c.nome
ORDER BY total_pedidos DESC;
💡 Ver Solução
-- Problema: Sequential Scan + JOIN sem índice
-- Solução: Criar índices

CREATE INDEX idx_clientes_cidade ON clientes(cidade);
CREATE INDEX idx_pedidos_cliente_id ON pedidos(cliente_id);

-- Agora rode EXPLAIN ANALYZE novamente
-- Deve ser 10-50x mais rápido!