Why I Use Raw SQL Instead of an ORM (Most of the Time)
This is going to be controversial, so let me start with the disclaimer: ORMs are fine. Prisma, SQLAlchemy, Drizzle — they're all good tools built by smart people. I use them.
But for the Nexural platform — 185 tables, complex joins, materialized views, row-level security — raw SQL was the right call for the critical paths. Here's why.
The Moment I Switched
I was using Prisma. The dashboard loaded in 200ms locally. In production with real data, it took 4.2 seconds.
I ran `EXPLAIN ANALYZE` on the generated query. Prisma was doing 6 separate queries where one JOIN would have worked. It was fetching entire rows when I needed 3 columns. And it was ignoring my carefully designed indexes because its query planner didn't know about them.
I replaced the Prisma query with raw SQL:
```sql -- What Prisma generated (simplified): 6 queries, 4.2 seconds SELECT * FROM strategies WHERE user_id = $1; SELECT * FROM positions WHERE strategy_id IN (...); SELECT * FROM trades WHERE position_id IN (...); -- ... 3 more queries
-- What I wrote: 1 query, 47ms SELECT s.id, s.name, COUNT(p.id) as position_count, SUM(p.unrealized_pnl) as total_pnl, MAX(t.executed_at) as last_trade FROM strategies s LEFT JOIN positions p ON p.strategy_id = s.id AND p.status = 'open' LEFT JOIN trades t ON t.strategy_id = s.id WHERE s.user_id = $1 GROUP BY s.id, s.name ORDER BY s.created_at DESC LIMIT 20; ```
4.2 seconds to 47ms. Same data. The ORM was making it slow, not the database.
When I Use an ORM
ORMs excel at:
CRUD operations. Creating a user, updating a profile, deleting a record — these are simple operations where the generated SQL is fine and the type safety is valuable.
Migrations. Prisma's migration system is genuinely excellent. I use it for schema management even when I write raw queries.
Prototyping. When I'm exploring a new feature and don't care about performance yet, ORM speed of development wins.
When I Use Raw SQL
Complex joins. Anything involving 3+ tables, aggregations, or window functions. ORMs either can't express these or generate inefficient queries.
Performance-critical paths. Dashboard loads, trading data queries, analytics aggregations. These run thousands of times per day and every millisecond counts.
Database-specific features. PostgreSQL materialized views, RLS policies, custom functions, CTEs (Common Table Expressions). ORMs abstract these away, but they're the most powerful tools in the database.
Reporting and analytics. Complex GROUP BY with HAVING, window functions like ROW_NUMBER and LAG, pivot queries. Writing these through an ORM is fighting the tool.
How I Keep Raw SQL Maintainable
The main argument against raw SQL is maintainability. Fair point. Here's how I handle it:
Typed query functions.
```typescript interface DashboardSummary { strategyId: string; name: string; positionCount: number; totalPnl: number; lastTrade: Date | null; }
async function getDashboardSummary(userId: string): Promise<DashboardSummary[]> {
const result = await db.query
The SQL lives inside a typed function. The caller doesn't know or care that it's raw SQL. If I need to change the query, I change it in one place.
SQL files for complex queries. For queries over 20 lines, I put them in .sql files and load them at build time. This gives me syntax highlighting, easier testing, and version control diffs that make sense.
Query tests. Every raw SQL query has a test that runs against a real database with test data. Not a mock — a real PostgreSQL instance. If my query has a syntax error or returns the wrong shape, the test catches it.
The Pragmatic Middle Ground
My actual split is:
- 70% ORM for standard CRUD, migrations, simple queries
- 30% raw SQL for dashboards, analytics, complex joins, performance-critical paths
This gives me the best of both worlds: fast development for simple stuff, and full control where performance matters.
The Hot Take
The "ORM vs raw SQL" debate is a false dichotomy. They're not competing tools — they're complementary. Use the ORM until it gets in your way, then drop to SQL for the specific queries that need it.
Anyone who says "always use an ORM" hasn't built a system with 185 tables. Anyone who says "never use an ORM" enjoys suffering.