12k
All articles

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.

OpenReplay Team
OpenReplay Team
ORM vs SQL Puro: Qual Deve Ser a Sua Escolha?

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 include do Prisma ou um JOIN) 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

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érioORM Completo (Prisma, TypeORM)Query builder (Drizzle, Kysely, Knex)SQL Puro
Segurança contra injeçãoParametrizado por defeitoParametrizado por defeitoSeguro apenas com instruções parametrizadas
Velocidade de desenvolvimento (CRUD)Mais rápida — relações e hidratação tratadas automaticamenteRápida — próximo do SQL, os joins são escritos pelo programadorMais lenta — mapeamento manual
Migrações / schemaSuporte nativo (Prisma Migrate, TypeORM)Integrado ou ferramenta complementar (Drizzle Kit, migrações Knex)Scripts DDL escritos manualmente
Portabilidade entre bases de dadosAlta — troca de providers com poucas alteraçõesMédia — com consciência de dialetoBaixa — SQL específico do dialeto
Controlo / funcionalidades específicas da BDLimitado; necessita de escape hatchAlto — próximo do SQLTotal — todas as funcionalidades, todas as hints
Segurança de tiposEnd-to-end a partir do schemaForte (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:

  1. 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.
  2. 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.
  3. 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.

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — self-hosted, with full data ownership.

Star on GitHub

We use cookies to improve your experience. By using our site, you accept cookies.