
SQL essencial para desenvolvedores: do SELECT à performance em 2026
Conteúdo educativo. Exemplos testados em PostgreSQL 16. Conceitos aplicáveis a MySQL 8+, SQL Server 2022 e SQLite 3.45+. Para Oracle, adapte sintaxe de LIMIT/OFFSET (ROWNUM/FETCH FIRST).
SQL é a linguagem que sobreviveu a 5 décadas de hype. NoSQL, document stores, vector DBs — todos coexistem, mas o modelo relacional continua sendo o padrão de fato para dados transacionais. O problema é que a maioria dos devs aprende SQL superficialmente: sabe fazer SELECT * e um INNER JOIN básico, mas trava quando o explain analyze mostra um seq scan em tabela de milhões de linhas. Este guia cobre o que realmente importa do dia a dia.
O modelo relacional em 60 segundos
Dados organizados em tabelas (relações), linhas (tuplas) e colunas (atributos). Chave primária identifica unicamente cada linha. Chaves estrangeiras criam vínculos entre tabelas. A integridade referencial garante que você não apague um cliente que ainda tem pedidos — a menos que programe um ON DELETE CASCADE.
| Conceito | Analogia | SQL |
|---|---|---|
| Tabela | Planilha | CREATE TABLE |
| Linha | Registro / tupla | INSERT / UPDATE |
| Coluna | Campo / atributo | SELECT coluna |
| Chave primária | CPF / UUID único | PRIMARY KEY |
| Chave estrangeira | Referência cruzada | FOREIGN KEY ... REFERENCES |
| Índice | Índice remissivo de livro | CREATE INDEX |
SELECT que não engasga
O SELECT é a instrução mais usada e a mais perigosa. SELECT * em tabela grande é um tiro no pé: trasfega dados desnecessários pela rede, aumenta IO e polui cache. Especifique colunas.
-- ruim: traz 47 colunas quando só precisa de 3
SELECT * FROM pedidos WHERE status = 'pendente';
-- bom: só o necessário, índice cobertor possível
SELECT id, cliente_id, total, created_at
FROM pedidos
WHERE status = 'pendente'
AND created_at > CURRENT_DATE - INTERVAL '7 days'
ORDER BY created_at DESC
LIMIT 50;Use EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) antes de otimizar. O PostgreSQL mostra custo estimado, tempo real, tipo de scan (sequential x index) e se houve temp files em disco.
JOINs explicados visualmente
JOIN é a operação mais mal-entendida por iniciantes. A regra mnemônica é: INNER JOIN = interseção, LEFT JOIN = tabela da esquerda + matches da direita, RIGHT JOIN = espelho do LEFT (raramente usado), FULL OUTER JOIN = união.
-- INNER JOIN: só clientes que fizeram pedidos
SELECT c.nome, p.total
FROM clientes c
INNER JOIN pedidos p ON p.cliente_id = c.id;
-- LEFT JOIN: todos os clientes, com ou sem pedido
SELECT c.nome, COALESCE(SUM(p.total), 0) AS total_gasto
FROM clientes c
LEFT JOIN pedidos p ON p.cliente_id = c.id
GROUP BY c.id, c.nome;
-- anti-join: clientes que NUNCA compraram (idioma LEFT JOIN + IS NULL)
SELECT c.nome
FROM clientes c
LEFT JOIN pedidos p ON p.cliente_id = c.id
WHERE p.id IS NULL;CROSS JOIN (produto cartesiano) explode o resultado: 1.000 clientes x 500 produtos = 500.000 linhas. Use só quando realmente precisar de todas as combinações. Self-join une uma tabela com ela mesma — útil para hierarquias (org_chart, árvore de categorias).
Agrupamento e filtros pós-agregação
GROUP BY quebra o resultado em subconjuntos. WHERE filtra linhas antes da agregação; HAVING filtra grupos depois. A ordem lógica de execução é: FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT.
-- top 10 clientes que gastaram mais de R$ 5.000 nos últimos 90 dias
SELECT c.nome, SUM(p.total) AS total
FROM clientes c
JOIN pedidos p ON p.cliente_id = c.id
WHERE p.created_at >= CURRENT_DATE - INTERVAL '90 days'
GROUP BY c.id, c.nome
HAVING SUM(p.total) > 5000
ORDER BY total DESC
LIMIT 10;Índices: quando funcionam e quando mentem
Índices aceleram leitura e desaceleram escrita (INSERT/UPDATE/DELETE precisam manter a estrutura ordenada). Um índice B-tree — o padrão — é eficiente para igualdade (=) e intervalos (>, <, BETWEEN). Não adianta criar índice em coluna com pouca cardinalidade (booleano, status com 3 valores).
| Tipo de índice | Quando usar | Exemplo |
|---|---|---|
| B-tree (padrão) | Igualdade, range, ORDER BY | CREATE INDEX ON pedidos(created_at) |
| Hash | Apenas igualdade (raro no PG) | CREATE INDEX ... USING hash |
| GiST | Dados geométricos, full-text | PostGIS, pg_trgm |
| GIN | Arrays, JSONB, full-text search | CREATE INDEX ... USING gin(jsonb_col) |
| BRIN | Dados ordenados sequencialmente grandes | Logs, time-series |
| Índice parcial | Filtrar subconjunto frequente | WHERE status = 'ativo' |
| Índice de expressão | Busca em função aplicada | LOWER(email) |
Índice cobertor (covering index) inclui colunas extras no nível folha, evitando lookup na tabela heap. Em PostgreSQL: CREATE INDEX idx ON tabela(col_a) INCLUDE (col_b, col_c).
-- verificar se um índice está sendo usado
EXPLAIN (ANALYZE, BUFFERS)
SELECT id, email FROM usuarios WHERE email = 'teste@exemplo.com';
-- índice funcional para busca case-insensitive
CREATE INDEX idx_usuarios_email_lower ON usuarios(LOWER(email));
-- índice parcial para pedidos pendentes (supõe que 95% já estão entregues)
CREATE INDEX idx_pedidos_pendentes ON pedidos(created_at)
WHERE status = 'pendente';Transações ACID e níveis de isolamento
Uma transação é uma unidade de trabalho indivisível: ou tudo acontece (COMMIT) ou nada acontece (ROLLBACK). ACID = Atomicidade, Consistência, Isolamento, Durabilidade. O isolamento é o mais sutil: dependendo do nível, leituras sujas, não-repetíveis e fantasmas podem ocorrer.
| Nível de isolamento | Dirty read | Non-repeatable | Phantom |
|---|---|---|---|
| READ UNCOMMITTED | Sim | Sim | Sim |
| READ COMMITTED | Não | Sim | Sim |
| REPEATABLE READ | Não | Não | Sim* |
| SERIALIZABLE | Não | Não | Não |
* PostgreSQL implementa REPEATABLE READ como snapshot isolation, que de fato previne phantoms na prática. SERIALIZABLE é o mais seguro, mas pode gerar serialization_failure — seu código precisa tratar e retry.
BEGIN;
UPDATE contas SET saldo = saldo - 100 WHERE id = 1;
UPDATE contas SET saldo = saldo + 100 WHERE id = 2;
-- se algo der errado antes do commit, faça ROLLBACK
COMMIT;CTEs e subqueries: legibilidade primeiro
Common Table Expressions (WITH) tornam queries complexas legíveis. Diferente de subquery inline, a CTE nomeia uma etapa intermediária. Atenção: no PostgreSQL, CTEs materiais são barreiras de otimização — o planner não consegue empurrar predicados para dentro delas. Use com moderação em queries críticas de performance.
WITH pedidos_recentes AS (
SELECT cliente_id, SUM(total) AS total
FROM pedidos
WHERE created_at >= CURRENT_DATE - INTERVAL '30 days'
GROUP BY cliente_id
HAVING SUM(total) > 1000
),
clientes_ativos AS (
SELECT id, nome
FROM clientes
WHERE status = 'ativo'
)
SELECT c.nome, pr.total
FROM clientes_ativos c
JOIN pedidos_recentes pr ON pr.cliente_id = c.id
ORDER BY pr.total DESC;Window functions: ranking e agregação por janela
Window functions computam sobre um conjunto de linhas relacionadas à linha atual (a "janela"), sem colapsar o resultado como GROUP BY faz. Útil para ranking, média móvel, comparar com anterior/próximo.
SELECT
categoria,
produto,
preco,
RANK() OVER (PARTITION BY categoria ORDER BY preco DESC) AS rank_categoria,
AVG(preco) OVER (PARTITION BY categoria) AS media_categoria,
preco - LAG(preco) OVER (ORDER BY preco) AS diff_anterior
FROM produtos;Normalização: até onde ir?
1NF elimina grupos repetitivos (nada de arrays ou CSV em células). 2NF elimina dependências parciais da chave. 3NF elimina dependências transitivas. A regra prática: normalize até 3NF, desnormalize com propósito quando a leitura for dominante e medida. Denormalização precoce é preguiça; normalização obsessiva é vício.
Exemplo de desnormalização justa: cache de total de pedidos por cliente em coluna calculada, atualizado por trigger ou batch, para evitar SUM() agregado a cada request do dashboard.
Erros que custam caro em produção
- Não usar LIMIT em SELECT exploratório em tabela grande — pode travar o servidor.
- UPDATE/DELETE sem WHERE: se o cliente de banco não tem safe mode, é desastre imediato. Sempre rode SELECT com a mesma cláusula WHERE antes de atualizar.
- SELECT COUNT(*) em tabela com milhões de linhas no PostgreSQL é lento (faz seq scan). Use pg_class.reltuples como estimativa ou contadores auxiliares.
- Lock escalation implícito: transações longas seguram locks, bloqueando writes concorrentes. Mantenha transações curtas.
- Não usar prepared statements em loops: parse e plan repetidos consomem CPU. Use prepared statements ou ORM com batch insert.
- N+1 queries: ORMs mal configurados disparam uma query por relacionamento. Use JOIN, select_related ou data loader.
Perguntas frequentes
+PostgreSQL ou MySQL para projetos novos em 2026?
PostgreSQL é o default recomendado: tipos avançados (JSONB, arrays, ranges), extensões ricas (PostGIS, pgvector), melhor conformidade com SQL padrão e licença permissiva (PostgreSQL License). MySQL 8+ é viável, especialmente se a infraestrutura já for Oracle/MySQL-first (ex: RDS barato). MariaDB divergiu do MySQL em features recentes.
+Quando usar NoSQL em vez de SQL?
Quando o modelo é documento naturalmente variável (logs, CMS headless), quando precisa de escalabilidade horizontal automática sem sharding manual (MongoDB Atlas, DynamoDB), ou quando o padrão de acesso é puramente chave-valor com baixa latência (Redis, ScyllaDB). Relacional continua sendo a escolha padrão para dados transacionais com integridade forte.
+O que é um deadlock e como evitar?
Deadlock ocorre quando duas transações seguram locks que a outra precisa. PostgreSQL detecta e aborta uma delas (deadlock_timeout, padrão 1s). Para evitar: acesse recursos sempre na mesma ordem, mantenha transações curtas e use locking pessimista (SELECT FOR UPDATE) só quando necessário.
+Como fazer backup seguro de PostgreSQL?
pg_dump para backups lógicos (SQL ou custom format). Para bases grandes (> 100 GB), prefira pg_basebackup (físico) ou ferramentas como WAL-G/Barman para PITR (point-in-time recovery). Teste a restauração periodicamente; backup que não foi testado é apenas esperança.
+Vale a pena aprender SQL antes do ORM?
Sim. ORM abstrai o SQL, mas gera queries horríveis quando mal utilizado. Saber SQL permite otimizar o que o ORM gera, criar views e stored procedures quando faz sentido, e entender planos de execução. Aprenda SQL primeiro; depois use ORM como acelerador, não como substituto.
Fontes consultadas
- PostgreSQL 16 Documentation
- Use The Index, Luke! (guia de performance SQL)
- SQL Antipatterns (Bill Karwin) — livro de referência
- PostgreSQL EXPLAIN visualizer (pgMustard)
- B-tree vs BRIN vs GIN (PostgreSQL wiki)
Revisão editorial: publicado em . Última revisão em . Conteúdo educativo, sem patrocínio das ferramentas citadas.
Leia também

Como Começar na Programação do Zero em 2026: Guia Definitivo
Roteiro completo para quem quer entrar na programação em 2026: escolha de linguagem, setup de ambiente, lógica de programação, primeiros projetos, comunidades e como evitar armadilhas comuns de iniciantes.

HTML, CSS e JavaScript: O Trio Essencial da Web Moderna
Domine os três pilares do desenvolvimento web: HTML semântico para estrutura, CSS moderno para estilo e layout responsivo, e JavaScript para interatividade, manipulação do DOM e consumo de APIs.

Git e GitHub Básico: Controle de Versão para Quem Coda
Guia prático de Git e GitHub para iniciantes: instalação, commits, branches, merge, rebase, pull requests, resolução de conflitos e fluxos de trabalho profissionais em equipe.