ORM vs SQL Puro: Qual Deve Ser a Sua Escolha?
ORM vs SQL puro: compare segurança, velocidade e controle, veja por que o N+1 pesa mais que a abstração e escolha a ferramenta certa para cada consulta.
Para a maioria das aplicações, a escolha padrão mais sensata é um ORM ou query builder para os 90% de operações CRUD rotineiras, e SQL escrito à mão para o pequeno conjunto de consultas de relatórios, análises e caminhos críticos de desempenho que realmente justificam esse esforço — não é uma decisão de um ou outro. A questão que realmente importa não é “qual é melhor em abstrato”, mas “qual é o mais adequado para esta consulta, nesta equipa, dados estes constrangimentos”. Este artigo fornece uma regra de decisão, uma tabela comparativa e o único ponto de falha — a consulta N+1 — que afeta equipas com muito mais frequência do que qualquer overhead de abstração alguma vez o fará. Os exemplos estão ancorados no ecossistema JS/TS (Prisma, Drizzle, Kysely, Knex), uma vez que é aí que a maioria destas decisões está a ser tomada atualmente.
Principais Conclusões
- Utilize por defeito um ORM ou query builder para operações CRUD rotineiras, e recorra ao SQL puro nas consultas específicas de relatórios, análises e caminhos críticos que comprovadamente o exigem — a maioria das aplicações em produção acaba por utilizar ambos.
- O verdadeiro custo de desempenho de um ORM quase nunca é a abstração em si; é o padrão de consulta N+1, em que carregar uma lista e depois as suas relações uma linha de cada vez transforma um único carregamento de página em centenas de round-trips.
- O problema N+1 resolve-se com eager-loading da relação numa única consulta (o
includedo Prisma ou umJOIN) e selecionando apenas as colunas que são renderizadas — não abandonando o ORM. - Query builders como Drizzle, Kysely e Knex são a terceira opção que a abordagem binária ignora: consultas com sintaxe próxima do SQL, verificadas por tipos, com controlo total do SQL gerado e sem camada de mapeamento de objetos.
- Os ORMs parametrizam as consultas por defeito, o que elimina o vetor de injeção SQL mais comum; o SQL puro é apenas tão seguro quanto a sua disciplina na utilização de instruções parametrizadas.
O que é um ORM e o que significa “SQL puro”
Um ORM (Object-Relational Mapper) é uma biblioteca que mapeia tabelas de base de dados para objetos na sua linguagem de programação, permitindo-lhe consultar dados através de chamadas a métodos e modelos tipados, em vez de escrever strings SQL manualmente. Gera o SQL, envia-o para a base de dados e hidrata as linhas de volta para objetos, tratando relações, parametrização e mapeamento de tipos ao longo do processo. No ecossistema JS/TS, o Prisma e o TypeORM são os exemplos de ORM completo. Uma leitura típica com Prisma tem este aspeto:
// Utilizadores e os seus posts, numa única chamada
const users = await prisma.user.findMany({
where: { posts: { some: { title: { contains: 'test' } } } },
include: { posts: true },
});
// users[0].posts já é um array tipado — sem necessidade de reformatação manual
SQL puro significa escrever o texto da consulta manualmente e executá-lo através de um driver de base de dados, reformatando depois as linhas do resultado plano na estrutura que a sua aplicação necessita. Tem controlo total sobre a consulta exata, mas também é responsável pelo mapeamento. A mesma leitura de “utilizadores com os seus posts” contra pg requer um join e um reagrupamento manual:
const { rows } = await pool.query(
`SELECT u.id, u.name, p.id AS post_id, p.title
FROM users u
LEFT JOIN posts p ON p.author_id = u.id
WHERE p.title LIKE $1`,
['%test%'],
);
// Linhas planas -> utilizadores aninhados, feito manualmente
const users = Object.values(
rows.reduce((acc, r) => {
acc[r.id] ??= { id: r.id, name: r.name, posts: [] };
if (r.post_id) acc[r.id].posts.push({ id: r.post_id, title: r.title });
return acc;
}, {} as Record<number, { id: number; name: string; posts: any[] }>),
);
Esse reduce é o verdadeiro custo do SQL puro para leituras relacionais: a base de dados devolve linhas, não grafos de objetos, e é necessário escrever a lógica de join e reagrupamento de cada vez. O placeholder $1 também está a fazer um trabalho importante — ver segurança, abaixo.
ORM vs SQL puro: a comparação que realmente decide
Discover how at OpenReplay.com.
A decisão resume-se a cinco critérios. Eis como as três abordagens — ORM completo, query builder e SQL puro — se comparam entre si:
| Critério | ORM Completo (Prisma, TypeORM) | Query builder (Drizzle, Kysely, Knex) | SQL Puro |
|---|---|---|---|
| Segurança contra injeção | Parametrizado por defeito | Parametrizado por defeito | Seguro apenas com instruções parametrizadas |
| Velocidade de desenvolvimento (CRUD) | Mais rápida — relações e hidratação tratadas automaticamente | Rápida — próximo do SQL, os joins são escritos pelo programador | Mais lenta — mapeamento manual |
| Migrações / schema | Suporte nativo (Prisma Migrate, TypeORM) | Integrado ou ferramenta complementar (Drizzle Kit, migrações Knex) | Scripts DDL escritos manualmente |
| Portabilidade entre bases de dados | Alta — troca de providers com poucas alterações | Média — com consciência de dialeto | Baixa — SQL específico do dialeto |
| Controlo / funcionalidades específicas da BD | Limitado; necessita de escape hatch | Alto — próximo do SQL | Total — todas as funcionalidades, todas as hints |
| Segurança de tipos | End-to-end a partir do schema | Forte (Kysely/Drizzle); best-effort (Knex) | Nenhuma sem ferramentas adicionais |
Em matéria de segurança, a verdade prática é simples: ORMs e query builders parametrizam valores por defeito, o que elimina o vetor clássico de injeção. O SQL puro é apenas tão seguro quanto a sua disciplina — a própria documentação do Prisma adverte que se utilizar o método unsafe com inputs do utilizador, abre a possibilidade de ataques de injeção SQL, que podem expor os seus dados a modificação ou eliminação. Utilize placeholders ($1, ?) e nunca interpole input do utilizador na string da consulta, e o SQL puro é seguro; abandone essa disciplina e não o será.
Em termos de migrações e portabilidade, os ORMs ganham no caso rotineiro. Uma alteração de schema torna-se uma migração versionada e repetível que é executada de forma idêntica em todos os ambientes, e a troca de providers de base de dados é maioritariamente uma alteração de configuração. O SQL puro não oferece nenhuma destas vantagens gratuitamente — é necessário gerir os scripts DDL manualmente e as suas consultas ficam vinculadas à sintaxe de um único dialeto. Em termos de controlo, o SQL puro vence de forma decisiva: window functions, CTEs recursivas, hints de índices específicos da base de dados e afinação com EXPLAIN ANALYZE estão todos disponíveis sem ter de lutar contra uma abstração.
A verdade sobre o desempenho: é o N+1, não a abstração
A crítica mais repetida aos ORMs — “geram consultas lentas” — está maioritariamente errada para CRUD e maioritariamente certa para um padrão específico. Os ORMs modernos geram SQL competente para leituras e escritas do dia a dia. O verdadeiro custo de desempenho é o problema da consulta N+1: carrega-se uma lista de N linhas e depois desencadeia-se mais uma consulta por linha para obter uma relação, transformando um único carregamento de página em N+1 round-trips à base de dados.
Eis o padrão de falha, que normalmente se esconde dentro de um ciclo de aparência inocente:
// 1 consulta para os utilizadores...
const users = await prisma.user.findMany();
// ...depois mais N, uma por utilizador — esta é a armadilha N+1
for (const user of users) {
user.posts = await prisma.post.findMany({ where: { authorId: user.id } });
}
A solução não é abandonar o ORM. Faz-se eager-loading da relação numa única consulta e selecionam-se apenas as colunas que são efetivamente renderizadas:
const users = await prisma.user.findMany({
include: { posts: { select: { id: true, title: true } } },
});
Isto colapsa 1 + N consultas numa só. O sintoma visível ao utilizador de um N+1 não corrigido é uma página de lista ou dashboard que é ágil com dados de seed e lenta em volume de produção — exatamente o tipo de latência no frontend que uma session replay evidencia do lado do utilizador, apontando de volta para a camada de consultas para contar queries e adicionar eager-loading.
Inspecione o SQL gerado antes de culpar a ferramenta
Antes de concluir que um ORM é lento, observe o que ele realmente emite. O Prisma regista cada consulta quando se instancia o cliente com log: ['query']; o Kysely expõe .compile() para devolver o SQL e os parâmetros; e o Drizzle expõe um método .toSQL() numa consulta para o mesmo efeito. De seguida, execute EXPLAIN ANALYZE no output para ver o plano que a base de dados realmente executa.
const prisma = new PrismaClient({ log: ['query'] });
// cada consulta que o Prisma executa é agora impressa — conte-as, copie-as para o EXPLAIN ANALYZE
Isto transforma “o ORM é lento” de uma perceção vaga num fluxo de trabalho inspecionável: leia o SQL, conte os round-trips, verifique o plano.
Uma nota sobre o próprio motor: uma das alterações mais significativas no Prisma 7 é a remoção completa do motor de consultas baseado em Rust em favor de uma implementação em TypeScript. O novo Query Compiler é executado em TypeScript e WebAssembly, o que elimina a necessidade do passo de serialização entre linguagens e resulta numa execução de consultas mais rápida; e como o motor já não depende de um binário nativo, é agora possível utilizar o Prisma em ambientes que suportam JavaScript ou WASM, como Cloudflare Workers, Bun e Deno. O Prisma reporta que a reescrita é frequentemente significativamente mais rápida onde mais importa — em consultas grandes e complexas — mantendo-se ao mesmo nível nas mais simples. O ponto relevante para esta comparação: o debate sobre o motor mudou de forma em vez de desaparecer, e continua a ser secundário relativamente à contagem de consultas. Para a esmagadora maioria das páginas lentas, o N+1 e o over-fetching são a causa, não a camada de abstração.
Query builders: a terceira opção que o enquadramento binário ignora
O enquadramento “ORM vs SQL puro” esconde um forte meio-termo. Query builders como o Drizzle, Kysely e Knex permitem escrever consultas com sintaxe próxima do SQL, verificadas por tipos, e manter controlo total do SQL gerado, sem a camada de mapeamento de objetos de um ORM. O Kysely é o query builder SQL com maior segurança de tipos para TypeScript, utilizado em produção pela Deno, Maersk e Cal.com, com TypeScript moderno e zero overhead em runtime. É uma camada de abstração fina sobre SQL, focada na familiaridade através da nomenclatura e estrutura, e na previsibilidade através de compilação 1:1.
const users = await db
.selectFrom('users')
.innerJoin('posts', 'posts.author_id', 'users.id')
.select(['users.id', 'users.name', 'posts.title'])
.where('posts.title', 'like', '%test%')
.execute();
A estrutura espelha o SQL, mas os nomes de colunas e tabelas são verificados por tipos face ao seu schema. Como a documentação do Kysely refere, um query builder oferece controlo total sobre o seu SQL enquanto garante que o TypeScript deteta erros precocemente, antes mesmo de o código ser executado. O Knex é a opção de longa data e funciona com Postgres, MySQL, SQLite e outros, mas tenha em atenção as suas limitações de tipagem: de acordo com o seu próprio guia, o suporte a TypeScript é best-effort, nem todos os padrões de utilização podem ser verificados por tipos, e a ausência de erros de tipo não garante atualmente que as consultas geradas estejam corretas, pelo que se recomenda a escrita de testes mesmo com TypeScript. Para um novo projeto em TS que pretenda segurança de tipos sem o peso de um ORM, o Kysely ou o Drizzle são a escolha mais sólida.
Uma regra de decisão por cenário
Adapte a ferramenta à carga de trabalho em vez de escolher uma para toda a base de código:
- CRUD simples a moderado, uma equipa e um schema em evolução → ORM. Obtém consultas parametrizadas, migrações versionadas, carregamento de relações e tipos end-to-end. Isto cobre a maior parte do código de aplicação.
- Relatórios, análises, operações em massa ou um caminho crítico medido → SQL puro, views ou stored procedures. Quando necessita de window functions, agregação complexa ou planos afinados manualmente verificados com
EXPLAIN ANALYZE, escreva o SQL diretamente. - Quer segurança de tipos e controlo do SQL sem um ORM → query builder. O Drizzle ou o Kysely oferecem consultas tipadas com sintaxe próxima do SQL, output previsível e sem comportamento N+1 oculto.
O híbrido em que a maioria das aplicações em produção acaba
Na prática, as bases de código maduras não escolhem um lado — utilizam um ORM para a maior parte do trabalho e mantêm um escape hatch de SQL puro para o resto. O Prisma torna isto explícito com o TypedSQL: uma funcionalidade do Prisma ORM que permite escrever consultas SQL puras de forma totalmente type-safe. Escreve-se SQL num ficheiro .sql e chama-se através de uma função gerada e tipada:
import { getUsersByAge } from './generated/prisma/sql';
const users = await prisma.$queryRawTyped(getUsersByAge(18, 30));
É uma funcionalidade em preview: ativa-se com previewFeatures = ["typedSql"] no bloco do generator e, por ser estaticamente tipada, pode não suportar determinados cenários, como cláusulas WHERE geradas dinamicamente — esses ainda necessitam dos métodos raw sem tipagem. O Prisma enquadra o fluxo de trabalho exatamente como este artigo o faz: uma abstração de nível superior que serve a maioria das consultas, mais um escape hatch type-safe para quando é necessário elaborar SQL diretamente. É por isso que a resposta moderna a “ORM ou SQL puro” é geralmente “um ORM (ou query builder) que permite escapar para SQL puro quando necessário.”
Conclusão
Escolha o ORM ou query builder como padrão para o CRUD do dia a dia, recorra ao SQL puro nas consultas que comprovadamente o justificam, e trate os dois como camadas numa única estratégia de acesso a dados em vez de campos rivais. O próximo passo concreto em qualquer projeto existente: ative o registo de consultas, encontre os endpoints de lista ou dashboard, conte as consultas por pedido e corrija os N+1s com eager-loading e uma seleção de colunas mais restrita — essa única passagem normalmente recupera mais desempenho do que qualquer reescrita de ORM para SQL alguma vez o faria.
FAQs
Qual é a diferença entre um ORM e um query builder?
Um ORM mapeia tabelas de base de dados para objetos e hidrata os resultados das consultas em modelos tipados, tratando relações e mapeamento de objetos automaticamente, enquanto um query builder gera SQL verificado por tipos mas devolve linhas simples sem uma camada de mapeamento de objetos. Prisma e TypeORM são ORMs; Drizzle, Kysely e Knex são query builders. Os query builders situam-se mais próximos do SQL, oferecendo maior controlo sobre a consulta gerada com menos abstração e sem comportamento oculto de carregamento de relações.
A utilização de um ORM previne a injeção SQL?
Sim, para utilização normal. ORMs e query builders parametrizam valores por defeito, enviando o input do utilizador como parâmetros vinculados em vez de o interpolar na string da consulta, o que elimina o vetor clássico de injeção. A exceção são os métodos de consulta raw não seguros: a documentação do Prisma adverte que utilizar o método unsafe com input do utilizador abre a possibilidade de injeção SQL. O SQL puro é apenas tão seguro quanto a sua disciplina na utilização de placeholders como dollar-one ou question-mark.
O SQL puro é mais rápido do que um ORM?
Não na maioria dos casos reais. Os ORMs modernos geram SQL competente para CRUD do dia a dia, e a diferença de desempenho habitual vem da contagem de consultas, não da abstração. A causa dominante de páginas lentas é o padrão N+1, em que carregar uma lista e depois as suas relações uma linha de cada vez cria centenas de round-trips. O SQL puro vale a pena ser utilizado em relatórios, análises e consultas de caminhos críticos medidos que genuinamente necessitam de planos afinados manualmente.
Como posso ver o SQL real que um ORM gera?
Cada ferramenta expõe o SQL gerado diretamente. O Prisma regista cada consulta quando se instancia o cliente com a opção de registo de consultas ativada. O Kysely expõe um método compile que devolve a string SQL e os seus parâmetros. O Drizzle expõe um método toSQL numa consulta para o mesmo efeito. Uma vez obtido o SQL, execute EXPLAIN ANALYZE no PostgreSQL para inspecionar o plano de execução e confirmar se os índices estão a ser utilizados conforme esperado.
Posso escrever SQL puro num projeto que usa ORM?
Sim, e é o padrão comum em produção. A maioria dos ORMs mantém um escape hatch de SQL puro ao lado das suas consultas geradas. O Prisma oferece o TypedSQL, uma funcionalidade em preview ativada com a flag de preview typedSql, que permite escrever SQL num ficheiro .sql e chamá-lo através de uma função gerada e totalmente tipada. Por ser estaticamente tipada, não suporta cláusulas WHERE geradas dinamicamente, que ainda requerem os métodos de consulta raw sem tipagem.