Go One Layer Deeper
One ordinary line of code, expanded downward. Every layer names what it hides for you, how it fails, and where to learn it properly. Descend as far as the problem requires — and stop.
await fetch('/api/users')SELECT * FROM users WHERE email = 'alice@exaawait userRepository.save(user)$ git push # …and it is liveagent.run("refund the duplicate charge for oawait auth.signIn(email, password)await stripe.charges.create({ amount: 4990 }open('out.txt', 'w').write(data)items.sort((a, b) => a.score - b.score)
One SELECT, all the way down
Depth
Declarative SQL says what you want. Everything between that and the bytes on disk is a decision the database made for you — and the reason the same query is fast on Monday and slow on Friday.
1SELECT * FROM users WHERE email = 'alice@example.com';- 1
1/10 layers · Enough to build
Why should an application engineer care?
You do not need to write a query planner. You do need to read what one decided, because "add an index" is sometimes right, sometimes useless, and sometimes the reason writes got slower.
The question this ladder asks
Why is this query actually fast or slow?
What are you delegating to an orm? →Where this goes wrong in production
- ORM lazy loading↓
- N+1 queries↓
- Database load↓
- Latency↓
- Production incident
What are you delegating here?
What are you delegating to an orm?
DatabaseYou write
1const user = await userRepository.findByEmail(email)The abstraction handles
- ✓SQL generation for your dialect
- ✓Parameter binding — which is what makes it injection-safe by default
- ✓Object ↔ row mapping and type coercion
- ✓Connection acquisition and release from the pool
- ✓Change tracking, so
save()writes only what changed
Still your responsibility
- →Query behaviour — what SQL this actually produces, and whether it is one statement or fifty
- →Index requirements — the ORM cannot create the index
findByEmailneeds; it will happily scan without it - →Transaction boundaries — where the transaction starts, ends, and what isolation level you got by default
- →N+1 risks — lazy loading turns attribute access into a query, invisibly, inside loops
- →Data consistency — cascades, orphans, and what a partial failure leaves behind
Know your escape hatch
When: The generated query is slow, the plan is wrong, or the shape of the read does not match any entity.
Drop to: Raw SQL for that query, and EXPLAIN to read what the database decided.
Keeping the ORM for 95% of the code and writing three hand-tuned queries is the normal outcome, not a failure of the ORM.