Why I stopped using ORMs for complex queries
A practical case for dropping the abstraction and writing SQL directly, and the tooling that makes it pleasant.
I used ORMs for years and I have no regrets about the early part of that. For a new project, for simple CRUD, they're genuinely fast. You model your schema, you get type-safe queries, you ship the feature and move on.
The problems show up later. And they always show up in the same place.
Where the abstraction breaks down
The trouble starts when requirements get real. Reporting queries. Analytical joins. Anything involving window functions, CTEs, or non-trivial aggregations. That's where you find yourself translating SQL you already understand perfectly into a fluent API that approximates SQL, and then debugging the difference between what you wrote and what actually ran against the database.
I've stared at ORM-generated queries that were technically correct and operationally terrible. Joins that should have been subqueries. N+1 patterns buried in a chain of method calls that looked reasonable in application code. The abstraction hides what is actually happening, which makes performance problems harder to diagnose.
What I do now
I write SQL. Then I generate types from it so the compiler catches mistakes.
const rows = await db.query(sql`
select u.id, u.name, count(o.id) as orders
from users u
left join orders o on o.user_id = u.id
where u.created_at > ${since}
group by u.id
`)Tools like pgtyped, kysely, and drizzle in query mode give you type inference from the actual query without building an abstraction on top of the query language itself. Your migrations are plain .sql files in version control. Anyone on the team can read them without knowing your ORM's API.
When I still reach for an ORM
Simple CRUD on a small surface. If you're building a form that reads and writes a few columns, the path of least resistance is fine and it really is faster. ORMs work well until they become the default answer to every question, including the ones they're bad at.
The abstraction should disappear when you don't need it. SQL is older than every framework you'll ever use. It outlasts them because it's actually good at what it does.