DEV Community

Mustafa ERBAY
Mustafa ERBAY

Posted on • Originally published at mustafaerbay.com.tr

ORM Tools Are Overrated: Why They Fall Short in Large-Scale Projects?

The Allure and Realities of ORM Tools

ORM (Object-Relational Mapping) tools are powerful instruments designed to reduce the complexity developers face when dealing with database operations. By bridging the gap between object-oriented programming paradigms and the world of relational databases, they allow developers to interact with database objects in their own programming languages instead of writing SQL queries. This abstraction layer can incredibly boost development speed, especially in small and medium-sized projects. Saving, updating, or deleting an object can be handled with a few lines of code, whereas the same operations might require writing complex SQL queries, managing connections, and manually processing results.

However, this development ease and speed increase can sometimes lead to serious performance bottlenecks and architectural issues in large-scale, high-performance enterprise projects. In my own experience, during the development of a large production ERP system and multiple enterprise software projects, I found that true performance optimizations began when we stepped outside the comfort zone provided by ORM tools. In this article, I will discuss why ORM tools are "overrated," why they fall short in large-scale projects, and how to tackle these situations with concrete examples.

The N+1 Problem: ORM's Classic Nightmare

One of the most well-known and frequently encountered problems with ORM tools is undoubtedly the N+1 query problem. This problem arises when fetching a collection of parent objects, causing separate additional queries to be triggered for each child object. For instance, when you fetch a list of orders and then want to retrieve the details for each order, the queries generated by the ORM can quickly escalate out of control. An operation starting with a simple SELECT * FROM orders; query then continues with N executions of queries like SELECT * FROM order_items WHERE order_id = ?; for each order.

Consider an e-commerce platform's order management module, where a page lists an average of 500 orders, and each order has an average of 10 order items. If the ORM operates with a lazy loading mechanism like fetch_related('items'), after fetching the main data for these 500 orders, it will fetch the items belonging to each order with a separate query. This means a total of 1 (main query) + 500 (order item queries) = 501 queries. With a simple calculation, the load these queries create on the database and the network traffic significantly increase the application's response time.

⚠️ The True Impact of the N+1 Problem

This situation directly impacts user experience, especially in systems like high-traffic e-commerce sites or financial transaction platforms. It can lead to slow page loads, "timeout" errors, and even database server overload. In a production ERP project I worked on, when listing all production orders for a month on an operator screen, a lazy load setting that fetched machine status for each order resulted in hundreds of queries, causing the page to take 20 seconds to load.

To solve this problem, we typically use the eager loading mechanisms offered by ORMs. Eager loading allows us to fetch related data along with the main query or with a single additional query. For example, in PostgreSQL, we can reduce this N+1 problem to a single query by using JOIN or combining multiple queries with UNION ALL. However, the queries automatically generated by an ORM can sometimes create complex JOINs, which makes database optimization more challenging.

Automatic Query Generation: The Hidden Enemy of Performance

Another critical issue with ORM tools is their automatic query generation mechanisms, which operate outside the direct control of the developer. ORMs, by looking at the developer's object model and database schema, can often generate inefficient or unexpected queries. This is a significant disadvantage, especially when the database schema is complex or when performance is critical. Developers may have to put in extra effort to understand and optimize the SQL queries generated by the ORM.

In an enterprise financial application, we were developing a reporting module that displayed users' balances in different currencies. The ORM was generating separate queries for each currency and each balance type, then combining these results in memory. Initially, this wasn't noticed because the data volume was low. However, as the amount of data increased, we saw how negatively this approach impacted performance. Fetching an average user's balance involved around 15-20 queries. When fetching balances for thousands of users in total, this number reached tens of thousands of queries.

-- Potentially inefficient query example generated by an ORM
SELECT
    u.id, u.name,
    (SELECT b.amount FROM balances b WHERE b.user_id = u.id AND b.currency = 'USD') AS usd_balance,
    (SELECT b.amount FROM balances b WHERE b.user_id = u.id AND b.currency = 'EUR') AS eur_balance,
    -- ... similar subqueries for other currencies
FROM
    users u
WHERE
    u.id IN (101, 102, ..., 500);
Enter fullscreen mode Exit fullscreen mode

This type of query causes the database to perform separate planning and execution for each subquery. In reality, this data could have been fetched much more efficiently with a single JOIN. However, ORMs often choose the simplest or most direct path, which may not always be the most performant. Therefore, even when using an ORM, it's crucial to regularly inspect the generated queries and, when necessary, resort to raw SQL queries or stored procedures.

ℹ️ Database Optimization: An Art

Database performance is not just about writing code; it's also about understanding database architecture, indexing strategies, and query optimization. ORMs can simplify this process, but for in-depth optimizations, it's often necessary to leverage the database's native capabilities. PostgreSQL's EXPLAIN ANALYZE command is your best friend for understanding the performance of queries generated by an ORM.

Complex Relationships and Database Schema Management

Large-scale applications often feature complex data models and numerous interconnected tables. This complexity can also pose a challenge for ORM tools. The mapping capabilities of ORMs might be limited, or managing complex relationship types (e.g., intermediate tables in many-to-many relationships, polymorphic relationships) can become difficult. This situation can force developers to bypass the abstraction provided by the ORM and deal directly with database operations.

In one project, we were developing a supply chain management system. In this system, many entities such as products, suppliers, orders, invoices, and stock movements were interconnected. Specifically, it was necessary to track products purchased from different suppliers at different prices. The ORM struggled to manage these many-to-many relationships with additional fields. For example, when we purchased a product from a specific supplier, we needed to add extra information specific to that relationship (e.g., purchase price, delivery time). The standard ManyToMany relationship offered by the ORM did not meet this need.

Consequently, for these complex relationships, we had to manually create intermediate tables and write custom SQL queries to manage the data within them. This meant deviating from the fundamental purpose of using an ORM. In such complex scenarios, the ORM either fell short or led to the generation of overly abstracted and difficult-to-understand code. In these situations, using raw SQL or simpler tools like a micro-ORM can improve both performance and code readability.

Performance Optimization: Going Beyond ORM

In large-scale projects, performance is critical not only for the application but also for the overall health of the database and infrastructure. While ORM tools can accelerate development, they often fall short when it comes to performance-oriented optimizations. At this point, developers need to have in-depth knowledge of database optimization and be able to go beyond the ORM.

One of the cornerstones of performance optimization is correct indexing strategies. ORMs can usually create indexes on basic fields, but specialized indexes optimized for complex queries or specific access patterns may be needed. For example, PostgreSQL's BRIN (Block Range INdex) indexes can be more efficient than traditional B-tree indexes in certain scenarios with large and ordered datasets. However, ORMs rarely automatically create or utilize such advanced index types.

At one point, I was working to speed up analytical queries for a large data platform. The queries generated by the ORM were scanning millions of rows using the LIKE operator on large tables. In my analysis with the EXPLAIN ANALYZE command, I saw that these queries were performing a full table scan and not benefiting from indexes at all. To solve the problem, I significantly improved search performance on text-based fields by using GIN (Generalized Inverted Index) indexes. These kinds of fine-tuning are far beyond the standard capabilities of an ORM.

💡 The Power of Raw SQL and Stored Procedures

While ORMs provide abstraction, using raw SQL queries or stored procedures on the database side often yields better results in performance-critical areas. This approach gives you full control over the query, allows you to better leverage the database's optimization capabilities, and can reduce network traffic. For instance, by moving a complex reporting query into a single stored procedure, you can improve both development and runtime performance.

Alternatives and Hybrid Approaches

After witnessing the shortcomings of ORMs in large-scale projects, developers and architects often turn to alternative approaches. These alternatives can offer more suitable solutions in specific scenarios, rather than completely abandoning the convenience provided by ORMs. This requires adopting a project-specific approach.

One approach is to continue using an ORM for certain parts of the project while employing raw SQL or simpler, micro-ORMs in performance-critical areas. For example, an ORM can be used for the application's user interface and basic data management operations, while specially optimized SQL queries or stored procedures can be used for the reporting module or heavily transactional background services. This hybrid approach allows us to maintain reasonable development speed while addressing critical performance bottlenecks.

Micro-ORMs (e.g., Dapper for .NET, PypyORM for Python) are much lighter than full-fledged ORMs and offer fewer layers of abstraction. They typically allow you to write SQL queries directly and map the results directly to objects. This gives the developer more control over the query while reducing the performance overhead introduced by ORMs. In large-scale systems, such tools can provide an excellent balance where performance is paramount.

For example, in a financial analysis platform, we were developing a module where users fetched and analyzed historical data. Fetching and processing this data with an ORM was very slow. Instead, we used a set of specially designed views in the database and a single complex SQL query. We then mapped the results of the query directly to data transfer objects (DTOs) with a micro-ORM like PypyORM. This resulted in an overall performance improvement of over 70% for the application.

Conclusion: The Necessity of a Balanced Approach

ORM tools hold a significant place in the software development world and are invaluable for accelerating development processes in many scenarios. However, in large-scale, high-performance enterprise projects, the abstraction and convenience offered by ORMs can lead to performance bottlenecks and architectural challenges. Problems like the N+1 issue, the inefficiency of automatic query generation, and the difficulty of managing complex data models can cause ORMs to be perceived as "overrated" in such projects.

My experience shows that to succeed in large-scale systems, it's essential to master topics like database optimization, indexing strategies, and raw SQL queries, rather than solely relying on ORMs. Adopting hybrid approaches, using different tools for different parts of the project, and going beyond the ORM in performance-critical areas often provide the most balanced and effective solution. Ultimately, choosing the best tool is possible by carefully evaluating the project's specific requirements, scalability goals, and performance expectations. It's important to remember that software architecture is often not a matter of preference, but the art of making the right trade-offs.

Top comments (0)