Every application that talks to a relational database has to decide how its code will express queries. One camp writes SQL by hand. The other uses an ORM (object-relational mapper), a library that lets developers work with database rows as objects in their programming language. Eloquent in Laravel, Doctrine in Symfony, Django's ORM, Entity Framework in .NET, Hibernate in Java and Prisma in Node.js are all examples. The ORM vs raw SQL debate can get heated, but for business applications the answer is rarely all one or the other. Here is how the trade-offs actually play out.
The same task, two ways
Suppose you need the ten most recent paid orders for a customer, with the customer's name. With raw SQL in PHP using PDO:
$stmt = $pdo->prepare('
SELECT o.id, o.total, o.created_at, c.name AS customer_name
FROM orders o
JOIN customers c ON c.id = o.customer_id
WHERE o.customer_id = :cid AND o.status = :status
ORDER BY o.created_at DESC
LIMIT 10
');
$stmt->execute(['cid' => $customerId, 'status' => 'paid']);
$orders = $stmt->fetchAll(PDO::FETCH_ASSOC);
With Laravel's Eloquent ORM, assuming the models and relationships are defined:
$orders = Order::with('customer')
->where('customer_id', $customerId)
->where('status', 'paid')
->latest()
->limit(10)
->get();
The ORM version is shorter and returns objects with useful behaviour attached. The SQL version is explicit: you can see exactly what reaches the database, and the result contains only the columns you asked for.
What an ORM gives you
- Speed of development. Common create, read, update and delete operations (CRUD) take one line instead of several. For admin screens and forms, this adds up quickly.
- Safety by default. ORMs use parameterised queries, which protects against SQL injection as long as developers do not bypass them with raw string concatenation.
- Relationships in code.
$order->customer->namereads naturally, and the ORM handles the join or lookup. - Migrations and schema history. Most ORMs come with migration tools that version schema changes alongside the code.
- Some database portability. Simple queries work across MySQL, PostgreSQL and SQLite, which helps with testing. In practice, real applications rarely switch databases, so this benefit is often overstated.
- Hooks and validation. Events such as "before save" let you centralise rules like setting timestamps or audit fields.
What raw SQL gives you
- Full power of the database. Window functions, common table expressions,
UPSERTvariants, full-text search and engine-specific features are all available without fighting an abstraction. - Predictable performance. You write the exact query, so there are no surprises about what runs.
- Efficient reporting. Aggregations over millions of rows belong in SQL. Loading thousands of objects into memory to sum them in application code is slow and wasteful.
- Shared language. Database administrators, analysts and developers can all read and tune the same SQL.
Where ORMs commonly cause trouble
The N+1 query problem
This is the classic ORM performance trap:
$orders = Order::where('status', 'paid')->get(); // 1 query
foreach ($orders as $order) {
echo $order->customer->name; // 1 query per order
}
With 500 orders, that is 501 queries. The code looks innocent, which is what makes it dangerous. The fix is eager loading, telling the ORM to fetch related records up front (with('customer') in Eloquent, select_related in Django, Include in Entity Framework). Many frameworks can be configured to warn about lazy loading during development.
Over-fetching
ORMs often select every column by default. Loading large text or JSON columns for a list page wastes memory and bandwidth. Most ORMs let you choose columns explicitly; developers just need to remember to.
Hidden complexity
Chains of scopes, relationships and conditions can generate SQL nobody would write by hand. Always be able to see the generated query, through a debug toolbar, query log or the ORM's "to SQL" method, and run EXPLAIN on anything slow.
Bulk operations
Updating 100,000 rows by loading each object, changing it and saving it individually is very slow. A single UPDATE ... WHERE statement does the same work in one round trip. Most ORMs offer bulk update methods, but they usually skip per-object events, which can surprise developers who rely on those hooks.
Where raw SQL causes trouble
- Injection risk if anyone builds queries by concatenating strings instead of using parameters.
- Repetition. The same joins and filters get copied into many places, and a schema change means hunting them all down.
- Mapping boilerplate. Converting rows into objects or data structures by hand is tedious and error-prone.
- Fragile dynamic queries. Building SQL for search screens with many optional filters by hand quickly becomes messy.
The middle ground: query builders
A query builder constructs SQL through method calls without mapping rows to full objects. Laravel's DB::table(), Knex.js, jOOQ and SQLAlchemy Core all sit here. They handle parameters and dynamic conditions safely while staying close to SQL. They are often the best fit for search screens and moderately complex reports.
ORM vs raw SQL: a practical rule for business apps
| Part of the application | Usually best served by |
|---|---|
| Forms, admin screens, standard CRUD | ORM |
| Search and filter screens | ORM or query builder |
| Reports, dashboards, exports | Raw SQL or database views |
| Bulk imports and data fixes | Raw SQL or bulk methods |
| Performance-critical hot paths | Whatever produces the best measured query |
Keep raw SQL in clearly named repository or report classes, always parameterised, rather than scattered through controllers. Our web application development team follows this mixed approach, and our database management service helps when generated queries start to strain the database.
Key takeaways
- In the ORM vs raw SQL decision, most business apps benefit from both.
- ORMs speed up everyday CRUD and are safe by default, but watch for N+1 queries and over-fetching.
- Raw SQL is the right tool for reports, aggregations and bulk changes.
- Whatever you use, make the generated SQL visible and check slow queries with
EXPLAIN.