Recommended Free Tools
If an app fetches a list of records and then loads a related object separately for every item, one apparent database operation can become dozens of queries. That pattern is the N+1 query problem: one query retrieves the starting set, and N additional queries retrieve related data. The fix is to inspect the SQL the app actually sends, then choose a loading strategy that fits the relationship and result size.
What the N+1 query problem means
Imagine an application fetching a list of 50 books. If the code then asks for each book’s author, and those author relationships have not already been loaded, the ORM may issue one query for the books and another for each author: 1 + 50 queries. The number 50 is just an illustration; the actual count depends on which relationships the code accesses, caching, query shape, and ORM behavior.
As an Amazon Associate I earn from qualifying purchases.
The “1” is the initial query for the parent records. “N” is the repeated query for related data across N records. The extra database work can be hidden behind ordinary-looking property access, so counting ORM calls in the source code does not necessarily reveal how many SQL statements run.
The Tool Desk
Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →How lazy loading creates repeated queries
Lazy loading means the ORM fetches a relationship when code first accesses it, rather than retrieving it with the original records. This can be efficient when related data is rarely needed. It becomes a problem when a loop accesses an unloaded relationship for many records: each access can trigger a separate SELECT.
#1 Best Overall
For example, a page might load a collection of books and then render each book’s author name. If the authors were not fetched in advance, rendering can trigger repeated queries. The loop itself is not necessarily the cause; the issue is the combination of repeated relationship access and relationships that are not already available.
There are exceptions. SQLAlchemy’s relationship-loading documentation notes that a simple many-to-one relationship may not need a new query if the referenced object is already present in the current session. Do not assume that every relationship access always results in a database round trip.
How to recognize N+1 in a query log
Look for an initial SELECT that fetches a set of parent rows, followed by many similar SELECT statements whose filter value changes to a different parent identifier each time. SQL logging or framework query inspection can expose this pattern and help trace which relationship access triggers it.
The SQLAlchemy 2.0 tutorial recommends testing the application, enabling SQL echoing, and observing the SQL emitted. The practical point is to diagnose the database traffic—not infer it from how compact the ORM code looks.
How to investigate and verify a fix
- Reproduce the slow request. Use representative data and the same application path that exhibits the problem.
- Count and inspect the statements. Review the SQL and its parameters to find repeated queries that differ mainly by a parent identifier.
- Trace the triggering access. Identify which relationship is read repeatedly and whether the application needs that related data for every item.
- Choose a targeted loading strategy. Retrieve the needed relationships with the initial query or with a batched secondary query, as appropriate.
- Compare under the same conditions. Check query count, rows returned, latency, and memory use. More queries do not automatically mean slower execution, and fewer queries do not guarantee a faster page.
Choose a loading strategy for the relationship
Eager loading retrieves related data as part of a planned query rather than waiting for each relationship access. It does not have one universally best SQL shape. The right choice depends on whether the relationship is scalar or a collection, the amount of data, the database backend, and the SQL actually generated.
| Strategy | What it does | When it may fit | Trade-off |
|---|---|---|---|
| Joined eager loading | Adds a JOIN so related values arrive in the same result set. | Often useful for a needed scalar relationship or when combined rows suit the query. | A join across a collection can multiply result rows. Inspect the result shape and execution plan. |
| Select-in or batched loading | Issues a secondary SELECT that fetches related rows for selected parent identifiers, often using an IN clause. | Often a straightforward choice for collections. SQLAlchemy describes select-in loading as simple and efficient in most cases. | It still uses more than one statement. Composite keys combined with a backend that lacks tuple-IN support can make this strategy infeasible. |
| Subquery eager loading | Loads related values with a secondary query based on a subquery. | May suit particular relationship and query shapes. | SQLAlchemy warns that the subquery can carry performance penalties depending on the database. |
| Raiseload guard | Raises an error when code attempts an unwanted lazy load. | Useful during development or on controlled paths to expose unexpected relationship access. | It is a guard, not a data-fetching optimization; explicitly configure relationships the code is meant to load. |
| Keep lazy loading | Fetches a relationship only when code accesses it. | Can be appropriate when related data is rarely needed. | Accessing the relationship in a loop over many records can create N+1 queries. |
A JOIN can reduce statement count but return duplicated parent data when a collection has multiple related rows. A batched query can avoid that row multiplication, though it adds a statement. Database plans, data volume, network latency, and caching all affect the trade-off; query count alone cannot establish which approach is faster.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.SQLAlchemy examples and version notes
In SQLAlchemy 2.1, relationship-loading options include lazy, select-in, joined, and raise loading. Common option names are selectinload(), joinedload(), and raiseload(). The SQLAlchemy 2.1 documentation calls eager loading the usual mitigation for N+1. Consult the documentation for the SQLAlchemy version in use before applying an option to a particular query.
Django’s official database-optimization guide discusses avoiding repeated database access when retrieving related pieces of a data set. Its exact API guidance should be checked against the Django version in use. Hibernate’s 5.0 documentation describes select fetching as especially vulnerable to N+1 selects; because that documentation is older, use the guide for the reader’s installed version before choosing Hibernate-specific APIs.
Keep important paths observable
For a code path where an unexpected relationship load would be costly, a guard such as SQLAlchemy’s raiseload() can turn an implicit query into an explicit failure. This helps reveal accidental access during development or on controlled paths; intended eager relationships still need to be configured deliberately.
Monitoring can also help identify potentially expensive lazy loads. The open-source nplusone project describes monitoring Django and SQLAlchemy for this pattern, but check its maintenance status and compatibility with your stack before adopting it.
Quick Recap
Sources and version-specific guidance
- SQLAlchemy 2.1: Relationship Loading Techniques
- SQLAlchemy 2.0 tutorial: Working with Related Objects
- Django 6.1: Database access optimization
- Hibernate ORM 5.0 documentation
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.




