The problem with ORMs is not that they exist.
The problem starts when a team treats the ORM as a database abstraction layer instead of a query generator that still needs to be understood, measured, and occasionally bypassed.
That distinction matters.
What ORMs Are Actually Good At
ORMs are good at:
- standard CRUD paths
- schema modeling
- migrations
- reducing repetitive mapping code
They are usually bad at hiding the cost model of relational databases.
If a team never looks at the SQL, the ORM becomes a latency generator with nice autocomplete.
The Failure Mode Everyone Eventually Sees
The classic example is the N+1 query problem:
const users = await db.user.findMany({ take: 50 });
for (const user of users) {
console.log(user.invoices[0]?.status);
}
At the application level, this looks harmless.
At the database level, it often means:
- one query to fetch users
- fifty more queries to fetch related records
That is not an ORM bug. It is the result of relationship access that hides query boundaries.
Eager Loading Is Not a Universal Fix
The usual reaction is to load everything up front:
const users = await db.user.findMany({
include: {
invoices: true,
},
take: 50,
});
That reduces round trips, but it can create a different problem:
- much larger result sets
- duplicated data across joins
- expensive in-memory reconstruction
So the real lesson is not "always eager load". It is "understand the query plan you just asked for."
When Raw SQL or a Query Builder Wins
Once a query becomes:
- aggregation-heavy
- join-heavy
- latency-sensitive
- report-like
you usually want to shape it explicitly.
A typed query builder is often a good middle ground:
const rows = await db
.selectFrom("users as u")
.leftJoin("orders as o", "o.user_id", "u.id")
.select(({ fn }) => [
"u.id",
"u.email",
fn.coalesce(fn.sum("o.total"), 0).as("lifetime_value"),
])
.groupBy(["u.id", "u.email"])
.execute();
The value here is not that SQL is morally superior. It is that you are forced to think about the shape of the query.
The Practical Rule
Use the ORM where it helps.
Drop lower when the query deserves first-class attention.
Good teams do both:
- ORM for routine data access
- explicit SQL for critical paths
- query logging and plan inspection for anything user-facing and slow
The engineering failure is not using an ORM. It is giving up responsibility for the SQL it emits.
Further Reading