SQL antes do ORM: o que o ActiveRecord, Eloquent e Hibernate escondem de você
O ORM escreve SQL por você — até travar a produção. Entender SQL de verdade separa quem usa o ORM de quem entende o que ele está fazendo.

Antes do Framework — Ep. 10
Você tem a arquitetura organizada, sabe como o código pensa e como ele se estrutura. O último ponto cego que a maioria dos devs carrega por anos: o banco de dados.
Todo framework popular tem um ORM. E o ORM é ótimo — até o dia em que você tem um sistema em produção lento, consultas que demoram 10 segundos, e o log de queries te mostra algo que você não faria nunca se tivesse escrito à mão.
O que o ORM faz (e o preço disso)
ORM significa Object-Relational Mapping. A ideia: você trabalha com objetos do seu código, e o ORM traduz para SQL.
# Django ORM
pedidos = Pedido.objects.filter(cliente=cliente).select_related('produto')
Isso gera um SQL por baixo. Às vezes o SQL que ele gera é exatamente o que você esperaria. Às vezes não.
O problema não é o ORM ser ruim. É não saber o SQL que ele gera e confiar cegamente no resultado.
SQL do zero: o que você usa todo dia
Vamos do começo. Um banco de dados relacional organiza dados em tabelas. Cada linha é um registro, cada coluna é um atributo.
-- criando a tabela de clientes
CREATE TABLE clientes (
id SERIAL PRIMARY KEY,
nome VARCHAR(100) NOT NULL,
email VARCHAR(150) UNIQUE NOT NULL,
criado_em TIMESTAMP DEFAULT NOW()
);
-- criando a tabela de pedidos
CREATE TABLE pedidos (
id SERIAL PRIMARY KEY,
cliente_id INT REFERENCES clientes(id),
total DECIMAL(10, 2) NOT NULL,
status VARCHAR(20) DEFAULT 'pendente',
criado_em TIMESTAMP DEFAULT NOW()
);
SELECT: buscando dados
-- todos os clientes
SELECT * FROM clientes;
-- só nome e email, com filtro
SELECT nome, email FROM clientes WHERE criado_em > '2026-01-01';
-- ordenado e limitado
SELECT nome, email FROM clientes ORDER BY criado_em DESC LIMIT 10;
JOIN: conectando tabelas
É aqui onde a maioria dos iniciantes trava. E é exatamente onde o ORM tende a gerar código ruim se você não entende o que está pedindo.
-- todos os pedidos com o nome do cliente
SELECT
pedidos.id,
clientes.nome,
pedidos.total,
pedidos.status
FROM pedidos
INNER JOIN clientes ON pedidos.cliente_id = clientes.id
WHERE pedidos.status = 'pendente'
ORDER BY pedidos.criado_em DESC;
INNER JOIN retorna apenas os registros que têm correspondência nos dois lados. LEFT JOIN retorna todos os registros da tabela da esquerda, mesmo que não haja correspondência.
-- todos os clientes, com ou sem pedidos
SELECT
clientes.nome,
COUNT(pedidos.id) AS total_pedidos
FROM clientes
LEFT JOIN pedidos ON pedidos.cliente_id = clientes.id
GROUP BY clientes.id, clientes.nome
ORDER BY total_pedidos DESC;
Agregações
-- total de pedidos por status
SELECT
status,
COUNT(*) AS quantidade,
SUM(total) AS valor_total,
AVG(total) AS ticket_medio
FROM pedidos
GROUP BY status;
-- clientes com mais de 5 pedidos
SELECT
cliente_id,
COUNT(*) AS quantidade
FROM pedidos
GROUP BY cliente_id
HAVING COUNT(*) > 5;
O problema do N+1: o bug mais comum que o ORM esconde
Você tem uma lista de pedidos e quer mostrar o nome do cliente de cada um.
Com ORM ingênuo:
# Django — parece inocente
pedidos = Pedido.objects.all()
for pedido in pedidos:
print(pedido.cliente.nome) # executa uma query por pedido!
Se você tem 100 pedidos, isso executa 101 queries: 1 para buscar os pedidos, e 1 para cada cliente. Com 1000 pedidos, são 1001 queries. É o problema N+1.
O ORM resolve com select_related (Django) ou eager loading (outros frameworks), que gera um JOIN:
# correto — 1 query com JOIN
pedidos = Pedido.objects.select_related('cliente').all()
Mas você só vai saber usar isso se entender o que está acontecendo por baixo. Sem esse conhecimento, você coloca N+1 em produção e só descobre quando o sistema fica lento com volume real de dados.
Como inspecionar o SQL que o ORM gera
Django:
# no shell do Django
from django.db import connection
print(connection.queries) # mostra todas as queries executadas
# ou diretamente na queryset
print(Pedido.objects.filter(status='pendente').query)
SQLAlchemy (Python):
import logging
logging.basicConfig()
logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO)
Spring Boot (Java):
# application.yml
spring:
jpa:
show-sql: true
properties:
hibernate:
format_sql: true
Coloque isso no ambiente de desenvolvimento e olhe o que aparece. Você vai se surpreender com o que o ORM gera em operações que parecem simples.
Quando usar SQL puro em vez do ORM
O ORM não cobre tudo com eficiência. Situações onde SQL direto faz mais sentido:
Relatórios e agregações complexas: Queries com múltiplos GROUP BY, HAVING, subconsultas e window functions ficam mais claras e eficientes em SQL puro.
Bulk operations: Inserir 10 mil registros de uma vez. O ORM faz isso linha por linha por padrão.
-- SQL puro — 1 operação
INSERT INTO pedidos (cliente_id, total, status)
SELECT id, 0, 'inativo' FROM clientes WHERE ativo = false;
Performance crítica: Quando a query precisa ser exatamente aquela, com os índices certos, sem abstração no meio.
# Django — SQL direto quando necessário
from django.db import connection
with connection.cursor() as cursor:
cursor.execute("""
SELECT cliente_id, SUM(total)
FROM pedidos
WHERE criado_em >= %s
GROUP BY cliente_id
HAVING SUM(total) > %s
""", ['2026-01-01', 5000])
resultado = cursor.fetchall()
Índices: o que você precisa entender antes de ir para produção
Um índice é uma estrutura auxiliar que acelera buscas. Sem índice, o banco percorre a tabela inteira para cada consulta. Com índice, vai direto.
-- sem isso, busca por email percorre a tabela inteira
SELECT * FROM clientes WHERE email = 'victor@exemplo.com';
-- com índice, é instantâneo mesmo com milhões de registros
CREATE INDEX idx_clientes_email ON clientes(email);
-- a coluna UNIQUE já cria índice automaticamente
-- índice em colunas usadas em JOIN e WHERE
CREATE INDEX idx_pedidos_cliente_id ON pedidos(cliente_id);
CREATE INDEX idx_pedidos_status ON pedidos(status);
Regra prática: toda coluna que aparece com frequência em WHERE, JOIN ou ORDER BY é candidata a índice.
O que muda quando você sabe SQL
Você não para de usar ORM. Você usa com mais consciência.
- Você sabe quando o ORM está gerando uma query ruim
- Você consegue escrever SQL direto quando o ORM não é suficiente
- Você entende
EXPLAIN ANALYZEpara debugar performance - Você não coloca N+1 em produção por acidente
- Você consegue revisar migrations sem depender de quem escreveu
Fechando a série
Dez episódios. A ordem que a maioria dos tutoriais ignora porque não é o caminho mais fácil de vender.
Redes → Linux → Docker e ambiente → Git em time → Infra como código → Stack → Paradigmas → Arquitetura MVC → SQL e banco de dados.
Agora você tem a base. A escolha do framework vai fazer sentido de um jeito diferente: não porque alguém disse que é o mais usado, mas porque você entende o que ele está resolvendo e o que ele está abstraindo.
O dev que entende sistemas escolhe a ferramenta certa. O dev que só conhece ferramentas depende de quem entende sistemas.
Bem-vindo ao segundo grupo.
→ Episódio bônus
Você tem a base. Agora vem a pergunta que paralisa a maioria: o que estudar a seguir? Existe uma ferramenta gratuita, visual e mantida pela comunidade que resolve exatamente isso — com roteiros por área, do básico ao avançado.
Antes do Framework — Ep. 11: Como montar seu roteiro de estudos com roadmap.sh
Newsletter
Em breveEm breve você poderá receber novos artigos direto no seu email.