The Pros and Cons of Using an ORM (Object Relational Mapping)
ORM (Object Relational Mapping) is one of the most popular approaches in modern application development. Almost all major frameworks — whether in Java, Ruby, Python, PHP, or Go — provide an ORM as the default choice for interacting with a database. However, behind the convenience and productivity it offers, an ORM also brings a number of consequences, especially in terms of query optimization. Many engineers have serious concerns about this: queries become hard to trace, hard to optimize, and sometimes their performance problems aren’t noticed until they reach production. This article isn’t written to reject ORMs, but to honestly understand when an ORM helps and when it becomes a burden.
What Is an ORM?
An ORM is a technique for mapping relational database tables into object form in a programming language. With an ORM, tables are mapped to classes, each row becomes an object, each column becomes an attribute, and relationships between tables (foreign keys) become object references or collections.
The difference is clearly felt in daily practice: without an ORM, developers write SQL manually for every operation; with an ORM, the same operation can be done via method calls like User.find(id) or accessing relationships via user.posts. The main goal of this approach is reducing repetitive SQL boilerplate, unifying data logic with application domain objects, and overall increasing developer productivity in writing database-related code.
Advantages of Using an ORM
High Developer Productivity
An ORM removes the need to write manual SQL for common operations like CRUD, pagination, and navigating table relationships. This benefit is especially felt on new projects still in the early phase, small teams with limited resources, or MVPs and prototyping prioritizing delivery speed. With an ORM, engineers can focus more on business logic, instead of spending time writing query details that are actually repetitive for standard operations.
Database Abstraction
An ORM makes an application more independent of a specific database vendor, and relatively easier to migrate from one database system to another — for example from MySQL to PostgreSQL — although there are still limits. As long as the application doesn’t use SQL features too specific to one vendor, an ORM helps maintain code portability against future database changes.
Security (Safer by Default)
Most ORMs automatically do parameter binding when composing queries, which significantly reduces SQL injection risk compared to manually writing SQL strings without consistent sanitization. For junior engineers who don’t yet fully understand the security risks of writing queries, this default behavior serves as a guardrail that greatly helps prevent fatal mistakes.
Data Structure Consistency
With an ORM, the data structure is defined in one place — usually a model or entity class — and relationships between tables become more explicitly visible in code, not hidden inside scattered SQL queries. This characteristic makes code review easier because reviewers can directly see the data structure from model definitions, and also speeds up onboarding for new engineers because they can understand the data schema through application code without directly reading the database schema.
Disadvantages of Using an ORM (The Often Underestimated Part)
Queries Hard to Trace (Hidden SQL)
This is the biggest concern in ORM usage. With an ORM, the SQL actually executed isn’t explicitly visible in code — queries are scattered across various methods, relations, hooks, or lazy loading mechanisms working behind the scenes. A concrete example: code like user.orders.items.product looks very simple on the surface, but can produce dozens of separate database queries without the author realizing it — a phenomenon known as the N+1 problem.
When performance becomes a problem, engineers must turn on the query logger, read the SQL generated by the ORM, then match those queries back to the high-level code lines that produced them. This process isn’t simple, even for senior engineers experienced with the ORM being used.
Query Optimization Becomes Difficult
Database optimization requires full control over the SQL being executed, understanding of the indexes used, and understanding of the execution plan (via EXPLAIN or similar commands). With an ORM, complex queries are often hard to express naturally through the provided API, and optimization efforts often collide with the abstraction the ORM itself builds.
As a result, two common scenarios occur: engineers “give up” and accept a suboptimal query to maintain code readability, or they force the ORM to produce an optimized query until the code eventually becomes unreadable — contradicting one of the ORM’s original goals.
Over-fetching and Under-fetching Data
ORMs tend to fetch more columns than actually needed, and sometimes fetch relationships not always required in a particular context. A simple example: if the application only needs the id and name columns, an ORM often still runs SELECT * by default, fetching all table columns.
At small scale, this difference isn’t very noticeable. But at large scale — high traffic, tables with many columns, or deep relationships — this pattern significantly impacts memory usage, network I/O between the application and database, and overall request latency.
The N+1 Query Problem
This is the classic problem almost always mentioned when discussing ORM disadvantages. A common pattern example: the application fetches 100 users, then for each of those users, the ORM automatically fetches each one’s orders separately.
sequenceDiagram
participant App as Application
participant ORM as ORM
participant DB as Database
App->>ORM: Fetch 100 users
ORM->>DB: 1 query: SELECT * FROM users
DB-->>ORM: 100 user rows
loop For each user (100 times)
ORM->>DB: SELECT * FROM orders WHERE user_id = ?
DB-->>ORM: That user's order data
end
ORM-->>App: Total: 101 queries executedWithout realizing it, this pattern produces one query to fetch user data, followed by a hundred separate queries to fetch each user’s orders — 101 queries total for an operation that conceptually should be completed with one or two queries via a JOIN or proper eager loading. This problem often isn’t visible in code review because the code looks simple and logically correct, and is only truly discovered when application load increases and the database starts showing signs of overload.
Debugging and Performance Tuning Are More Expensive
Without an ORM, the SQL being executed is clearly visible directly in the code, so performance bottlenecks are usually quickly found by looking at the query in question directly. With an ORM, the debugging process requires additional tracing, mapping from generated SQL back to the application code that triggered it, and understanding of the internal ORM mechanisms being used — like how lazy loading works or when queries are actually executed. This combination of extra steps significantly increases debugging time compared to writing SQL explicitly.
False Sense of Simplicity
ORMs often give the illusion that “a database is just object storage” — as if interacting with a database is as easy as manipulating ordinary objects in memory. In reality, databases have their own, far more complex cost model: indexes, joins, aggregation, and locking all significantly affect performance, and none of them are truly hidden just because they’re wrapped in an ORM abstraction.
Engineers who work only through an ORM for too long without ever touching SQL directly risk becoming weak in understanding SQL itself and database design in general. A comfortable abstraction can mean these core abilities are never truly trained, even though those abilities are still needed the moment serious performance problems appear.
The Impact of ORMs on Query Optimization Over Time
An ORM’s impact on query optimization isn’t constant — it changes as the application grows, and understanding this change pattern is important for determining when a strategy needs adjustment.
flowchart TD
A[Application is still small] --> B[ORM is very helpful, performance problems rarely appear]
B --> C[Data and traffic grow]
C --> D[Queries start slowing down, analysis gets harder]
D --> E[An incident occurs]
E --> F[Hard to answer: where does this query come from?]
F --> G[The ORM changes from a helper into a liability]When an application is still small, an ORM is very helpful and performance problems rarely appear because the data volume and traffic aren’t yet large enough to expose the inefficiencies that actually exist. As data and traffic grow, queries start slowing down, analysis of the causes becomes harder, and small changes in application code can produce very different SQL without anyone noticing. When an incident actually happens, the most fundamental question — where in the code does this query come from — becomes hard to answer quickly, and making an emergency hotfix becomes more complicated than if the SQL were written explicitly. At this phase, the ORM often changes role, from a helper accelerating development into a liability that actually slows down response to production problems.
When Is an ORM Appropriate?
ORM IS SUITABLE if:
✓ The application is CRUD-heavy with low query complexity
✓ The team consists of many junior engineers
✓ The main focus is speed of delivery
✓ Performance isn't yet a main bottleneck right now
These four conditions are interrelated — they all point to contexts where productivity and onboarding ease are more valuable than granular control over every executed query.
When Should an ORM Be Limited or Avoided?
CONSIDER NOT GOING FULL ORM if:
✗ Queries are complex and need deep optimization
✗ There are many reporting or aggregation needs
✗ High traffic and very sensitive to latency
✗ Full control over the SQL being executed is needed
In these contexts, the abstraction an ORM offers becomes an obstacle instead of a help, because the main need is precision and control, not speed in writing simple CRUD code.
The Hybrid Approach (the Most Realistic)
Many mature database-management teams don’t choose one approach extremely, but consciously combine both: an ORM is used for simple CRUD operations and write operations that don’t need special optimization, while raw SQL or a query builder is used for read-heavy queries, reporting needs, and performance-critical queries.
This hybrid approach provides two advantages at once: the team stays productive for the majority of daily operations that are indeed simple, while still having full optimization control for the parts of the system that truly need high performance. There’s no obligation to choose one fully — both can coexist in the same codebase, chosen based on the specific needs of each system part.
Best Practices If Still Using an ORM
If you decide to keep using an ORM as the main approach, these practices help reduce the risks already discussed:
- Enable query logging in non-production environments to make N+1 problem detection easier from the development phase
- Still make understanding SQL and reading
EXPLAINmandatory, even if using an ORM daily - During code review, also check the generated queries, not just the high-level application code
- Avoid lazy loading as the default for relationships that are almost always needed
- Use explicit select fields instead of relying on
SELECT *by default - Separate the read model and write model if query complexity for each side is already sufficiently different
These six practices essentially point to one same principle: don’t let the ORM abstraction block visibility into what’s actually happening at the database level.
Summary
- An ORM maps tables to objects — classes, rows, columns, and relationships are mapped into a more natural structure in application code, increasing developer productivity.
- Main advantages: high productivity, database abstraction for portability, default security via parameter binding, and data structure consistency.
- Main disadvantages: hidden and hard-to-trace SQL, query optimization becomes difficult, data over-fetching/under-fetching, the N+1 query problem, and more expensive debugging.
- An ORM’s performance impact changes as the application grows — very helpful when small, but becomes a liability when traffic and data grow.
- An ORM suits CRUD-heavy applications with junior teams and a speed-of-delivery focus; consider limiting it for complex queries, reporting, and latency-sensitive systems.
- The hybrid approach — an ORM for simple CRUD, raw SQL for critical queries — is the most realistic choice for many teams.
- Still master SQL and
EXPLAINeven when using an ORM — a database remains a database, not just an object store.