Decision guide
ORM vs Query Builder vs Raw SQL
How should application code talk to a relational database: a full ORM, a query builder, or SQL with code generation?
Side by side
| Criterion | Full ORM | Query builder | Raw SQL with code generation |
|---|---|---|---|
| Productivity for CRUD | Highest | Medium | Lower (unless generated) |
| Control over SQL | Lowest (escape hatches available) | High | Full |
| Type safety | Model-level | High in typed builders | High with code generation |
| Common failure mode | N+1 queries and over-fetching | Re-implementing ORM features | String-built SQL injection without parameters |
| Complex queries | Awkward; drop to raw SQL | Good | Best |
| Migrations | Usually built in | Separate tool | Separate tool (Flyway, Atlas, golang-migrate) |
When to choose each
Full ORM
Maps tables to classes with relationships, change tracking and migrations (Django ORM, SQLAlchemy ORM, EF Core, Hibernate, Eloquent, Prisma).
Choose when
- CRUD-heavy business apps with many entities
- Teams that want migrations, relations and validation integrated
- Rapid development with conventions
Avoid when
- Complex reporting queries and heavy SQL tuning
- When hidden queries (lazy loading, N+1) would hurt and nobody watches query counts
Query builder
Composable, often type-safe SQL in the host language without object mapping (Kysely, Drizzle, Knex, jOOQ, SQLAlchemy Core).
Choose when
- Dynamic queries (filters, sorting) built in code
- Type safety with SQL-like control
- Teams comfortable with SQL who dislike ORM magic
Avoid when
- You want identity maps, change tracking and relationship loading done for you
Raw SQL with code generation
SQL written in .sql files or strings, with tools generating typed functions from the queries (sqlc, PgTyped, SQLDelight) or a thin driver.
Choose when
- Performance-critical paths and complex queries (CTEs, window functions)
- Teams with strong SQL skills
- Go services and data-heavy backends
Avoid when
- Lots of dynamic query composition
- Teams that would end up hand-writing CRUD for dozens of tables
Verdict
Use the ORM that comes with your framework for typical business apps, and learn to see the SQL it generates (query logs, N+1 checks). Use a query builder when queries are dynamic and you want type safety without object mapping. Use SQL with code generation for performance-critical or SQL-heavy services. Mixing is normal: an ORM for CRUD plus hand-written SQL for reports, always with parameters, never string concatenation.
Learn more
- Scaling Databases & Data Consistency
- ASP.NET Core Web APIs
- Django from Basics to Production
- Databases & Transactions
- Performance & Scalability
- Speed up EF Core reads with projections, no-tracking and pooling (ASP.NET Core)
- Avoid N+1 queries with select_related and prefetch_related (Django)
- Run database transactions with database/sql (Go)
- Catch N+1 queries with strict Eloquent mode (Laravel)