{"post":{"slug":"orm-pitfalls-explained-2026","title":"ORM Pitfalls: The Queries You Did Not Know You Wrote","excerpt":"An ORM generates SQL from code that does not look like SQL. Most performance problems in ORM-backed applications come from that gap rather than from the ORM being slow.","category":"code","date":"January 30, 2026","author":"ByteLedger Team","content":"<span class=\"dropcap\">A</span>n ORM lets you write code that reads like objects and produces SQL you never see. That is the point, and it is why performance problems in ORM-backed applications are so consistently surprising — the expensive operation does not look expensive in the code.\n\nNearly all of these problems are visible immediately once you look at the generated SQL, which is why the single most valuable habit is logging it.\n\n## What changed in 2026\n\n- **Query logging in development became standard.** Frameworks made it easier to see generated SQL, which surfaced problems earlier.\n- **N+1 detection tooling spread.** Automated detection of repeated similar queries in a request became widely available.\n- **Explicit loading strategies gained favour.** Configuring relationships as lazy by default and loading explicitly became the recommended pattern.\n- **Query counting in tests appeared.** Asserting on the number of queries a code path executes became a practical regression guard.\n\n## The N+1 problem\n\nThe dominant issue, and it looks entirely reasonable in code.\n\nFetch a list of orders. Loop over them. Access each order's customer. Each access triggers a query, because the customer was not loaded. One hundred orders becomes one hundred and one queries.\n\nIn code that is a loop with a property access. In the database it is a hundred round trips, each with its own latency, which dominates everything.\n\nThe fix is loading the related data up front — a join or a second query fetching all the customers at once. Every ORM supports this and the default is usually not to do it, because the ORM cannot know whether you will access the relationship.\n\nThe reason it survives to production is that it is invisible at small scale. Ten orders in development feels fine; ten thousand in production does not — see [GraphQL N+1](/blog/graphql-n-plus-one-2026) for the same problem at an API layer.\n\n## Over-fetching\n\n| Pattern | What it does |\n| --- | --- |\n| Load the entity to read one field | Fetches every column |\n| Load a collection to count it | Materialises rows to count them |\n| Load a collection to check emptiness | Materialises rows to check existence |\n| Load with all relationships eagerly | Fetches far more than needed |\n\nSelecting whole entities is the default, and it fetches every column including large text or JSON ones. On a table where large values are stored out of line, that means an additional fetch per row for data you are discarding — see [TOAST storage](/blog/toast-storage-explained-2026).\n\nCounting by loading is worse than it looks: the database can count without materialising rows, and loading a collection to call length on it transfers everything to count it.\n\nChecking whether a collection is empty has the same shape, and existence checks are among the cheapest queries a database can run.\n\n## Transactions and connections\n\nTwo subtler problems.\n\n**Transaction scope wider than necessary.** An ORM operation wrapped in a transaction that also performs an external API call holds a database transaction open across a network round trip. That blocks cleanup and holds locks for the duration — see [MVCC explained](/blog/mvcc-explained-2026).\n\n**Connection held across the request.** Some configurations hold a connection for a whole request rather than for the queries within it, which multiplies connection demand — see [connection limits](/blog/connection-limits-explained-2026).\n\nBoth are configuration and code-structure issues rather than ORM defects, and both are common.\n\n## Making problems visible\n\n**Log generated SQL in development.** The single highest-value habit. Most of these problems are immediately obvious when you see twenty near-identical queries for one page load.\n\n**Count queries in tests.** Asserting that a code path executes a bounded number of queries catches N+1 regressions before they ship, and it is one of the cheapest performance guards available.\n\n**Watch query statistics in production.** A trivial query with an enormous call count is the signature — see [pg_stat_statements](/blog/pg-stat-statements-2026).\n\n**Do not abandon the ORM for raw SQL by reflex.** The ORM is not the problem for the overwhelming majority of queries. Use raw SQL for the few that genuinely need it, and keep the ORM for everything else.\n\n## Common mistakes\n\n- **Lazy loading in a loop.** The classic N+1.\n- **Loading entities to read one field.** Fetches everything.\n- **Counting by materialising.** The database counts far better.\n- **Eager-loading everything to avoid N+1.** Over-fetches instead.\n- **Never looking at generated SQL.** All of this stays invisible.\n- **Wide transaction scope.** Holds locks across slow operations.\n- **Rewriting everything in raw SQL.** Loses the ORM's benefits for a few queries' sake.\n\n## FAQ\n\n**Should I use an ORM at all?**\nFor most applications, yes — the productivity and safety benefits are real, and the problems here are avoidable once you know them. The alternative is writing and maintaining a great deal of SQL by hand.\n\n**How do I find N+1 in an existing codebase?**\nLog queries per request and look for repeated similar queries. Detection tooling automates this. Production query statistics also show it as a high call count on a trivial query.\n\n**Is eager loading always better?**\nNo — it over-fetches when the relationship is not used. Load explicitly where you know you need it, rather than configuring everything eager.\n\n**What about raw SQL for reports?**\nReasonable. Complex analytical queries are where ORMs struggle most, and dropping to SQL there while keeping the ORM elsewhere is a sensible split.\n\n## Where to go next\n\nFor the API-layer version of the same problem, read [GraphQL N+1](/blog/graphql-n-plus-one-2026). For finding these in production, [pg_stat_statements](/blog/pg-stat-statements-2026), and for the fetching cost of large columns, [TOAST storage](/blog/toast-storage-explained-2026).","hue":306,"metaTitle":"ORM Pitfalls: The Queries You Did Not Know You Wrote","metaDescription":"The common ORM performance problems — lazy loading in loops, over-fetching entities, counting by loading — and how logging generated SQL exposes them.","keywords":["orm pitfalls","n plus one queries","lazy loading problem","over fetching","orm performance","generated sql","eager loading","application performance 2026"],"tldr":["**Lazy loading in a loop is the dominant problem** — one query becomes N, silently.","**Selecting whole entities fetches columns you do not need** — expensive with large columns.","**Counting by loading a collection is common and terrible** — the database can count without materialising.","**Log generated SQL in development** — most problems are obvious the moment you see the queries.","**Skip:** blaming the ORM. It generated what the code asked for; the code asked badly."],"readTime":"4 min read","wordCount":927},"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":"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","readTime":"5 min read","hue":318},{"slug":"how-to-optimize-database-queries-2026","title":"How to optimize database queries in 2026: a measured approach","excerpt":"Most slow queries come from missing indexes and bad query shapes, not the database engine. Here is how to measure, fix, and verify query performance.","category":"code","date":"May 22, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":222}],"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":"latin-america-economic-rise-2026","title":"Latin America's economic rise in 2026: a new era of stability?","excerpt":"Latin American sovereigns held up better than most of the developed world through 2024–2026's volatility. Here is the plain-English read on what changed, what didn''t, and how to think about exposure.","category":"finance","date":"January 30, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":285},"older":{"slug":"python-uv-package-manager-2026","title":"Python uv in 2026: the fastest package manager and why you should switch","excerpt":"uv replaced pip, pipenv, and virtualenv in our Python workflow in 2026. Here is what it does differently, how fast it actually is, and the one gotcha to know before switching.","category":"code","date":"January 30, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":305}}