| slug: | orm-vs-raw-sql |
|---|---|
| date: | Mar 10, 2026 |
| readTime_en: | 6 min read |
| readTime_pt: | 6 min leitura |
| title_en: | Why I stopped using ORM and went back to raw SQL |
| title_pt: | Por que deixei de usar ORM e voltei ao SQL direto |
| excerpt_en: | After years of Sequelize and Prisma, I hit a wall with complex queries. Here's what I learned switching back to pg and writing SQL by hand. |
| excerpt_pt: | Depois de anos com Sequelize e Prisma, cheguei a um limite com queries complexas. O que aprendi ao voltar ao pg e escrever SQL à mão. |
| tags: | Node.js, PostgreSQL, Backend |
I've used Sequelize, TypeORM, and Prisma across different projects. For simple CRUD apps they're great — you get type safety, migrations, and a nice API out of the box. But once your queries grow beyond a few joins, things start to fall apart.
The turning point for me was a reporting query at Dragonboat. It involved multiple CTEs, window functions, and conditional aggregations. The Prisma version was a wall of $queryRaw with TypeScript fighting me every step. The raw SQL version was clean, readable, and 3x faster.
I moved to postgres (the sql package by porsager). It gives you tagged template literals with automatic parameterisation, so SQL injection is still handled correctly:
const users = await sql`
SELECT id, name, email
FROM users
WHERE active = true
AND created_at > ${since}
ORDER BY created_at DESC
LIMIT ${limit}
`;
No magic. No hidden N+1 queries. No wondering what SQL your ORM is actually generating.
The one thing ORMs genuinely do well is migrations. I replaced that with db-migrate for structured migrations and a simple schema.sql file as source of truth. Not glamorous, but it works.
For anything else? Write the SQL. You'll thank yourself later.
---pt---
Já usei Sequelize, TypeORM e Prisma em vários projectos. Para apps simples de CRUD são óptimos — tens type safety, migrações e uma API agradável logo de início. Mas quando as queries crescem para além de alguns joins, as coisas começam a correr mal.
O ponto de viragem foi uma query de relatório no Dragonboat. Envolvia múltiplos CTEs, window functions e agregações condicionais. A versão com Prisma era um muro de $queryRaw com TypeScript a lutar contra mim em cada passo. A versão com SQL directo era limpa, legível e 3x mais rápida.
Mudei para o postgres (o pacote sql do porsager). Oferece template literals com parametrização automática, por isso a injecção de SQL continua a ser prevenida correctamente:
const users = await sql`
SELECT id, name, email
FROM users
WHERE active = true
AND created_at > ${since}
ORDER BY created_at DESC
LIMIT ${limit}
`;
Sem magia. Sem queries N+1 escondidas. Sem dúvidas sobre o SQL que o ORM está realmente a gerar.
A única coisa que os ORMs fazem genuinamente bem são as migrações. Substituí isso por db-migrate para migrações estruturadas e um simples ficheiro schema.sql como fonte de verdade. Não é glamoroso, mas funciona.
Para tudo o resto? Escreve o SQL. Vais agradecer a ti mesmo mais tarde.