Why I Stopped Using ORMs (and When I Still Do)
TL;DR: ORMs solve a problem most apps don't have. Plain SQL with a thin query builder beats Prisma/SQLAlchemy/Sequelize for most production workloads. But there's a real case where ORMs win.
The case against
ORMs hide SQL behind an API. That's fine until you need to optimize a query — and then you're translating ORM-speak back into SQL anyway, plus fighting the abstraction.
The N+1 problem is built into every ORM. Lazy loading is an antipattern that ORMs make ergonomic. user.posts.first.comments.first.author.name is six queries dressed as one expression.
What I use instead
Plain SQL with a parameterized query builder. In Python, that's asyncpg + tagged-template SQL or SQLAlchemy core (not the ORM). In TypeScript, it's Kysely or Drizzle.
// kysely — query builder, no ORM
const users = await db
.selectFrom("users")
.where("active", "=", true)
.select(["id", "email", "created_at"])
.orderBy("created_at", "desc")
.limit(10)
.execute();
// users is fully typed as { id: number; email: string; created_at: Date }[]
When ORMs still win
Admin panels. Internal tools. CRUD-heavy apps with no perf-critical paths.
Schema migrations. Even when I write SQL by hand for queries, I let the ORM (or its migration tool) generate migration files.
Auto-generated APIs. Hasura, PostgREST, Prisma+tRPC — all of these need an ORM-style metadata layer.
The hidden cost of leaving
Every team I've moved off an ORM has had a 2-week period where everyone re-learns SQL. That cost is real. Don't migrate unless you have at least one person on the team who already writes SQL fluently.
A middle path
Use the ORM's query builder, not its model layer. SQLAlchemy's core API is a query builder; SQLAlchemy's ORM is a model layer with sessions, relationships, and lazy loading. The first is great. The second is what most 'I hate ORMs' rants are actually about.