Why I stopped using ORMs for complex queries
After two decades of using every abstraction layer available, I've come to a simple conclusion: ORMs are excellent for 80% of your queries. The remaining 20% — the joins, the aggregations, the window functions — deserve raw SQL.
This isn't a hot take. It's an observation from building production systems where query performance matters. When you're running 50,000 requests per minute, a poorly optimized ORM query becomes very visible very quickly.
Where ORMs earn their keep
For straightforward CRUD — inserts, simple selects, updates by primary key — an ORM is genuinely better than raw SQL. The model definitions serve as documentation. Migrations are handled consistently. You get type safety for free.
The mistake is stretching this tool beyond what it was designed for.
Where raw SQL wins
The moment you write .include() on a related model inside a loop, or you notice your ORM generating a correlated subquery where a simple join would do, you've crossed the threshold.
Window functions are the clearest example. ROW_NUMBER() OVER (PARTITION BY ...) in SQL is ten words. The equivalent in most ORM query builders is either impossible or a string escape hatch anyway.
The pattern that works
ORM for writes and simple reads. Raw SQL for analytics, complex aggregations, and anything that touches more than two tables non-trivially.
Clear separation, no magic, no surprises.