Blog /desenvolvimento

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.

9 min
Antes do Framework — série sobre fundamentos de desenvolvimento

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 ANALYZE para 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

Gostou do artigo?

Newsletter

Em breve

Em breve você poderá receber novos artigos direto no seu email.