n-plus-one-finder
Skill0 starsChanged 4 months agoMITView on GitHub
Find database queries running inside loops — the N+1 pattern that is invisible on seed data and quadratic in production — plus unbounded queries with no limit and repeated identical reads in one request. Use when a page is slow, before a launch, after adding a list view, or when database load rises without traffic rising.
In this repository19 other files it gives its agents.
- client-secret-sweepSkill
- codebase-onboarding-mapSkill
- commitSkill
- crawlability-auditSkill
- dependency-outage-auditSkill
- deploy-drift-checkSkill
- empty-state-auditSkill
- env-drift-checkSkill
- fake-data-guardSkill
- honest-metrics-auditSkill
- migration-safety-reviewSkill
- money-math-reviewSkill
- prompt-injection-reviewSkill
- pr-reviewerSkill
- release-notes-writerSkill
- retry-safety-reviewSkill
- route-auth-sweepSkill
- timezone-reviewSkill
- webhook-safety-reviewSkill
The file
We split this skill into the rules it gives an agent
4 rules. About 0 are common, 3 are distinctive, and 1 is very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈3
Distinctive · estimate
This author's own choices. Worth a look.
1
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 56
Read the ones inside render loops or list mappers.
- Distinctive: line 136
Never add an index as the fix for N+1.
- Distinctive: line 138
If a page's query count is constant as rows grow, it is not N+1.
- Distinctive: line 144
Keep this line and others can find it too.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
N+1 Finder
Install
Save this file as ~/.claude/skills/n-plus-one-finder/SKILL.md, or
.claude/skills/n-plus-one-finder/SKILL.md to scope it to one repo. Claude Code
auto-discovers it. Invoke with /n-plus-one-finder or by asking "why is this page
slow?".
Why this exists
N+1 is the most common serious performance bug in application code and the easiest to ship, because it is invisible exactly where you develop. With 10 seed rows it costs 11 fast local queries and nobody notices. With 10,000 rows and network latency it is 10,001 round trips and the page times out.
It is also the bug that scales with your success. It appears the week traffic arrives, which is the week you can least afford to be debugging it.
The tell is structural, not intuitive: a query inside an iteration. Find those and you have found most of it.
Step 1 — Find queries inside loops
rg -n -B6 "await (prisma|db|knex|sequelize|supabase|pool)\." | rg -B6 "(\.map\(|\.forEach\(|for \(|for await|while \()"
rg -n "\.map\(async"
rg -n "for (const|await const).*\{[\s\S]{0,300}?await .*(find|select|query|get)" -UThen read each hit, because the grep over-reports. You are confirming three things:
- A query genuinely executes per iteration.
- The collection is unbounded, or bounded only by something that grows.
- The data could have been fetched in one query instead.
Promise.all around per-item queries is not a fix. It converts a serial N+1
into a parallel one: still N queries, now arriving simultaneously, which exhausts
the connection pool instead of the clock. It usually looks like the most optimised
code in the file.
Step 2 — Find the lazy-relation version
The subtler form has no visible query. An ORM lazily loads a relation when a field is touched, so the loop looks like plain property access:
rg -n "\.(products|items|orders|user|author|seller|comments|tags)\b" --type-add 'src:*.{ts,tsx,js,rb,py}' -tsrcRead the ones inside render loops or list mappers. In Prisma this is usually a
missing include/select; in ActiveRecord a missing includes; in SQLAlchemy a
missing joinedload. The fix is to fetch relations with the parent, once.
Step 3 — Find unbounded reads
A query with no limit is a time bomb whose fuse is your own growth:
rg -n "findMany\(|SELECT |\.all\(|\.filter\(" -A4 | rg -v "take:|limit|LIMIT|first:|\.take\("Flag any list read on a table that grows without bound (orders, events, logs,
messages, downloads) and has no take/LIMIT. Fine at 30 rows, fatal at 300,000,
and the transition is silent.
Pay attention to reads that then discard most of what they fetched: selecting every row to count them, or to find one. Push that work into the database.
Step 4 — Find repeated identical reads in one request
Different from N+1: the same row fetched several times while rendering one page, usually because separate components each resolve the current user or the same product.
rg -n "getCurrentUser\(\)|findUnique\(\{ where: \{ id" -tsrcCount how many times a single request path can hit each. The fix is request-scoped
memoisation (React cache(), a DataLoader, or a per-request map), not another
index.
Step 5 — Measure, do not guess
Static reading identifies candidates. Only measurement ranks them.
Turn on query logging and load one real page:
# Prisma
new PrismaClient({ log: ["query"] })
# then count what one request emitsTwo numbers per page: total queries, and whether that total grows with the number of rows displayed. The second is the whole diagnosis. A page issuing 40 fixed queries is worth a look. A page issuing 3 + 1-per-item is the bug, even if it is faster today.
Seed enough rows to see it. Testing N+1 against 5 records proves nothing.
Step 6 — Report
Pages/endpoints measured: <n>
N+1 CONFIRMED
<file:line> <what loops> -> 1 + N queries, N = <what grows it>
measured: <x> queries at <y> rows
fix: <single include/join/batched fetch>
PARALLEL N+1 (Promise.all over queries)
<file:line> -> N concurrent queries, exhausts the pool rather than the clock
UNBOUNDED READS
<file:line> <table> no limit -> grows forever
REPEATED IDENTICAL READS
<file:line> <n>x per request -> memoise per requestGive each entry the query count at two different row counts. "12 queries at 10 rows, 102 at 100" ends the argument immediately; "possible N+1" starts one.
Rules
- Confirm by reading; the greps over-report by design.
Promise.allover N queries is still N queries. Say so explicitly.- Rank by measured growth, not by how bad the code looks.
- Never add an index as the fix for N+1. The problem is the number of round trips, not the speed of each.
- If a page's query count is constant as rows grow, it is not N+1. Say it is fine and move on.
From Toolbay. Free to use, modify, and share. Keep this line and others can find it too.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

