N plus one queries
Skill Amey-Thakur/AI-SKILLS/skills/performance/n-plus-one-queries
Plug-and-play skills and prompts for every AI coding agent
npx -y skills add Amey-Thakur/AI-SKILLS --skill n-plus-one-queriesAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- 19 days oldThe repository was created 19 days ago. New is not bad, but a brand new repository carrying a familiar-sounding name is the shape a typosquat arrives in, and there has been no time for anyone else to find a problem with it.
- 4 stars4 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Find and eliminate N+1 database access where one query per row replaces one query for the set, using eager loading, batching, or a dataloader. Use when a list endpoint is slow, query counts scale with result size, or an ORM lazy-loads inside a loop.
SKILL.md
2.7 KB, as published. Nobody here has run it
N+1 queries
The N+1 pattern issues one query to fetch a list, then one more query per row to fetch a relation: 1 + N round trips where 2 would do. Each query looks fast in isolation, so the plan never flags it; the cost is the hundreds of network round trips the loop hides. It is the most common reason a list page crawls.
Method
- Count queries per request, not query duration. Turn on your ORM's query
log (Rails
ActiveRecordlog, Djangodjango-debug-toolbar, Hibernateshow_sql, Prismalog: ['query']) and load one realistic page. If the count grows with the number of rows returned, you have N+1. - Trace it to the relation accessed inside the loop. The tell is a lazy
association read while iterating:
order.customer.namefor each order in a list. The first query fetched orders; each.customerfires its own. - Fix owned relations with eager loading. Load the association in the same
round trip: Rails
includes(:customer), Djangoselect_related/prefetch_related, SQLAlchemyselectinload, Prismainclude. This turns 1 + N into 1 or 2 queries, using a join or a singleWHERE id IN (...). - Batch when eager loading does not fit. For relations resolved across
services or resolvers, collect the keys and issue one
INquery per batch. In GraphQL, a per-request DataLoader coalesces the individual field reads into one keyed batch and caches within the request. - Verify the query count dropped, and watch the swing. Eager loading a
huge fan-out relation can replace N small queries with one enormous join
that materializes more rows than you want. Compare rows examined before and
after; prefer a second
INquery over a cartesian join when the fan-out is wide. - Add a regression guard. Assert a query-count ceiling in a test
(
assert_queries(2),django-assert-num-queries) so the next lazy access inside a loop fails CI instead of shipping.
Signals
- Does the query count stay flat as the result set grows from 10 to 100 rows?
- Is every association read in a loop covered by an eager load or a batch?
- Does a test pin the query count for the hot list endpoint?
Boundaries
This targets access-pattern round trips, not slow individual queries; a single query that is slow belongs to sql-optimization. When the batched query itself is expensive, precomputation via materialized-views or a read cache may be the better lever.