{"post":{"slug":"lazy-loading-vs-eager-loading-orm-2026","title":"Lazy Loading vs Eager Loading in Your ORM (2026)","excerpt":"The wrong ORM loading strategy turns one page view into hundreds of queries. Here is how lazy and eager loading actually differ, and how to choose per query instead of guessing.","category":"code","date":"August 6, 2026","author":"ByteLedger Team","content":"<span class=\"dropcap\">L</span>azy loading fetches a related record only at the moment your code actually accesses it; eager loading fetches related records upfront, alongside the rows that reference them. Both are valid strategies, and the choice is not \"lazy is bad, eager is good\" — it is about matching the strategy to what a specific piece of code actually does with the data. The trouble is that every major ORM defaults to lazy loading for individual relationships, and looping over a list of rows while lazily accessing each one's relationship issues one query per row. That pattern, known as the N+1 query problem, is the single most common ORM-caused performance bug in production applications, and it is entirely avoidable once you know where to look for it.\n\n## What changed in 2026\n\n- **Prisma's relation-loading strategy matured**, with `relationJoins` now stable and the query engine defaulting to a single SQL join for most `include` calls instead of separate round-trip queries.\n- **Drizzle's relational query API** made eager loading a first-class, type-safe query builder feature rather than an ORM-specific afterthought bolted onto raw SQL.\n- **SQLAlchemy 2.0-style loading strategies** became the norm across the Python ecosystem, with `selectinload` largely replacing older, chattier eager-loading patterns for collections.\n- **Automatic N+1 detection shipped in more tooling** — several APM vendors and ORM-adjacent libraries now flag suspiciously repetitive query patterns at runtime instead of leaving it to manual profiling.\n\n## How the two strategies actually execute\n\nLazy loading defers the query: you fetch a list of orders, and only when your code reads `order.customer` does the ORM issue a fresh query for that one customer. Do that inside a loop over 200 orders and you get 1 query for the orders plus 200 more for their customers — 201 total for data that could have been fetched in 2 queries. Eager loading fetches the relationship at the same time as the parent rows, either through a SQL `JOIN` (one query, wider rows) or a second batched query using `WHERE id IN (...)` across all the parent IDs at once (two queries, no duplication). Both eager strategies scale with the number of *distinct* queries, not the number of rows, which is what makes them immune to the N+1 pattern.\n\n## Eager loading syntax across common ORMs\n\n| ORM | Eager load call | Strategy under the hood |\n| --- | --- | --- |\n| Prisma | `include: { customer: true }` | Join or batched query, engine-dependent |\n| Django ORM | `select_related()` / `prefetch_related()` | JOIN for to-one, batched IN query for to-many |\n| SQLAlchemy | `joinedload()` / `selectinload()` | JOIN or batched IN query, chosen per relationship |\n| TypeORM | `relations: { customer: true }` | JOIN by default |\n| Drizzle | `with: { customer: true }` | Batched relational query |\n\nThe to-one versus to-many distinction matters: a `JOIN` for a to-many relationship duplicates the parent row once per child, which can be worse than a second batched query once the child count grows past a handful of rows.\n\n## How to choose per query\n\n1. **Default to lazy loading for relationships you rarely access.** Paying the eager-loading cost on every query for a relationship you touch 1% of the time wastes more than it saves.\n2. **Eager-load anything you access inside a loop over rows.** If the code pattern is \"for each row, read a relationship,\" that is exactly the shape that turns into N+1 under lazy loading.\n3. **Prefer a batched IN query over a JOIN for to-many relationships** once the child count is more than a handful, to avoid row duplication in the result set.\n4. **Measure with your ORM's query log or an APM tool**, not intuition — count actual queries per request, not queries per line of code.\n5. **Re-check after refactors.** Adding a new loop over an existing lazy relationship is the most common way N+1 problems reappear months after the code was first written cleanly.\n\n## Common mistakes\n\n**Looping over a relationship without checking how it was loaded.** The code reads perfectly fine; the query count only shows up in a slow query log or an APM trace, often in production, well after code review.\n\n**Eager-loading every relationship on every query.** Blanket `include` statements fetch data the request never uses, bloating response size and sometimes running slower than the N+1 pattern they were meant to avoid.\n\n**Mixing JOIN-based eager loading with a to-many relationship that has many children.** A user with 500 orders eagerly joined produces 500 duplicated copies of that user's row in the raw result set before the ORM deduplicates it back in memory.\n\n**Assuming eager loading is free.** It trades query count for payload size and, in the join case, for duplicated data transferred over the wire — worth it for the common loop case, wasteful for data you access conditionally.\n\n## FAQ\n\n**How do I detect an N+1 problem in an existing codebase?**\nTurn on your ORM's SQL query logging in a staging environment, hit the suspect endpoint once, and count queries. A page that issues one query per item in a list, scaling with list length, is the signature.\n\n**Is eager loading always faster than lazy loading?**\nNot for every case — for relationships accessed rarely or conditionally, lazy loading avoids fetching data the request never needs. Eager loading wins specifically when you know you will access the relationship for every row.\n\n**Do GraphQL resolvers have this same problem?**\nYes, arguably worse, because each field can resolve independently per row. The standard fix is a request-scoped batching layer, commonly a DataLoader, which coalesces per-row lookups into one batched query per tick.\n\n**Can I mix lazy and eager loading in the same query?**\nYes — most ORMs let you eager-load specific relationships while leaving others lazy on the same query, which is exactly how you should be using them: eager for what the loop needs, lazy for everything else.\n\n## Where to go next\n\nSee [how PgBouncer pools the connections your ORM opens](/blog/pgbouncer-explained-2026), review [database migration strategies for 2026](/blog/database-migration-strategies-2026) for keeping schema changes compatible with whichever loading strategy your ORM uses, and check [how to pick a database in 2026](/blog/how-to-pick-a-database-2026) if the underlying data model is still an open question.","hue":318,"metaTitle":"Lazy Loading vs Eager Loading ORM Guide 2026","metaDescription":"Lazy loading vs eager loading in 2026: how each ORM strategy works, the N+1 query storm, and per-ORM syntax for Prisma, Django, and SQLAlchemy.","keywords":["lazy loading vs eager loading","orm loading strategy","n+1 query problem orm","eager loading prisma","select_related prefetch_related","sqlalchemy joinedload","orm query optimization 2026","lazy loading orm explained","batch loading orm"],"tldr":["**Lazy loading fetches related data only when you access it** — convenient in code, but it silently issues one query per row when you loop over a relationship.","**Eager loading fetches related data upfront**, in the same query or one extra batched query, trading a little overhead for a predictable query count.","**The N+1 query storm is lazy loading's signature failure mode** — one query to fetch rows, then N more queries for each row's relationship.","**Every major ORM defaults to lazy loading** — Django, SQLAlchemy, ActiveRecord, and TypeORM all require you to opt into eager loading explicitly per query.","**Skip:** eager-loading every relationship on every query \"to be safe\" — that over-fetches data you don't need and can be slower than the N+1 it was meant to prevent."],"readTime":"5 min read","wordCount":1043},"related":[{"slug":"n-plus-one-query-problem-2026","title":"The N+1 Query Problem Explained in 2026","excerpt":"The N+1 query problem quietly turns one query into hundreds by firing a fetch per row inside a loop. Here is how to spot it, and the fixes that actually work.","category":"code","date":"July 2, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":280},{"slug":"what-is-an-orm-2026","title":"What is an ORM in 2026? A plain-language guide for developers","excerpt":"An ORM is a library that lets you work with a database using your programming language objects instead of raw SQL. This guide explains how ORMs work, their trade-offs, and when to reach for one.","category":"code","date":"June 1, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":160},{"slug":"how-to-optimize-sql-queries-2026","title":"How to optimize SQL queries in 2026: indexes, plans, and real fixes","excerpt":"Slow SQL is rarely a mystery — the query plan tells you exactly what is wrong. This 2026 guide covers the tools and techniques to find and fix slow queries fast.","category":"code","date":"April 1, 2026","author":"ByteLedger Team","readTime":"5 min read","hue":309}],"sameCategory":[{"slug":"bazel-build-explained-2026","title":"Bazel Explained: What Hermetic Builds Actually Cost","excerpt":"Bazel delivers reproducible, precisely cached builds and demands that you describe everything. Here is what you get, what it costs, and who should say no.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":145},{"slug":"graphql-federation-2026","title":"GraphQL Federation: One Graph, Many Teams","excerpt":"Federation composes independently-owned subgraphs into a single API. Here is what it solves, the operational cost it adds, and when a simpler gateway is enough.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":300},{"slug":"hermetic-builds-explained-2026","title":"Hermetic Builds Explained: Same Inputs, Same Output","excerpt":"A hermetic build depends only on declared inputs. Here is what that property gives you, the common leaks that break it, and how to move toward it incrementally.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":130},{"slug":"monorepo-tooling-compared-2026","title":"Monorepo Tooling Compared: What You Actually Need","excerpt":"Monorepo tools solve task orchestration and caching, not repository layout. Here is what each tier provides and how to tell when you have outgrown the simple option.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":150}],"newer":{"slug":"layoff-preparation-guide-2026","title":"Preparing for a Layoff Before It Happens","excerpt":"The work that makes a layoff survivable is done while you still have a job. Here is what to put in place and what the warning signs look like.","category":"guides","date":"August 6, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":44},"older":{"slug":"leader-election-explained-2026","title":"Leader Election Explained in 2026: How It Works","excerpt":"Leader election lets a cluster agree on a single coordinator without splitting into two. This guide explains how the mechanism works and why most systems delegate it to etcd or ZooKeeper.","category":"code","date":"August 6, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":316}}