Skip to content
Back to skills

N Plus One Detector

ASecurity

MANUAL-ONLY; never auto-invoke. Detect and design out chatty data-access patterns — the N+1 (one query per row), repeated identical queries in one request, serial awaited calls in loops, and over-fetching. Detection is evidence-first: per-request query counts and logs, ORM instrumentation, static cues (lazy relation touched inside iteration); the fix is pattern-matched — eager/preload, batched IN-key or dataloader-style loads memoized per request scope, joins or denormalization with prices st...

  • 4 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added September 11, 2026
businessgogitapidatabaseperformance

Works with

  • api

Security analysis

A100/100

Pro scans all 4 files and shows the line behind each finding

Scanned September 28, 2026

npx -y skills add ModernNomad-98/Project-Aegis --skill n-plus-one-detector --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of N Plus One Detector?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for N Plus One Detector
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/modernnomad-98-n-plus-one-detector/badge)](https://www.skillsdirectory.com/skills/modernnomad-98-n-plus-one-detector)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: n-plus-one-detector
description: "MANUAL-ONLY; never auto-invoke. Detect and design out chatty data-access patterns — the N+1 (one query per row), repeated identical queries in one request, serial awaited calls in loops, and over-fetching. Detection is evidence-first: per-request query counts and logs, ORM instrumentation, static cues (lazy relation touched inside iteration); the fix is pattern-matched — eager/preload, batched IN-key or dataloader-style loads memoized per request scope, joins or denormalization with prices stated — plus the regression guard: query-count budgets asserted in tests so the pattern cannot return. Each individual query is usually FAST — that is why plan-reading finds nothing; the defect is the pattern. Use when a request issues suspiciously many queries, latency scales with result-set size, or lazy-load storms are suspected. Do NOT use for one slow query (query-plan-reader), unattributed slowness (profiling-methodology-designer), or caching design (caching-strategy-designer)."
disable-model-invocation: true
---

# N-Plus-One Detector

## Purpose

The N+1 is the most reliably re-invented performance defect in software:
the list renders, each row lazily touches a relation, and a 50-row page
issues 51 queries — every one of them fast, the sum of them slow, and the
whole thing invisible to single-query tooling because no single query is
guilty. This skill detects chatty data-access patterns with evidence
(query counts per request, logs, and object-relational mapper (ORM)
instrumentation), designs the pattern-matched fix (eager loading, batching,
joins, denormalization —
each with its price), and specifies or, when implementation is authorized,
installs the regression guard: a query-count
budget asserted in tests, because N+1s return the moment someone adds a
field to a template. Latency that scales with result-set size is this
skill's signature symptom.

## Use When

- Use when: a request, page, or job issues suspiciously many queries —
  a query log, application performance monitoring (APM) trace, or count
  shows tens-to-hundreds of similar statements per operation.
- Use when: latency scales with result-set size — 10 items fast, 100
  items 10× slower — the arithmetic signature of per-row access.
- Use when: an ORM's lazy loading is suspected of firing inside loops,
  serializers, or templates.
- Use when: designing the regression guard — query-count budgets in
  tests for endpoints that have been burned before.
- Use when: serial awaited calls in a loop (per-item remote/DB round
  trips) need batching design, even where no ORM is involved.
- Do NOT use when: ONE query is slow — its plan is the evidence and
  `query-plan-reader` the tool; here every query is typically fast.
- Do NOT use when: nothing has attributed the slowness yet — "the app
  is slow" starts at `profiling-methodology-designer`; a query-count
  explosion in its findings is what routes here.
- Do NOT use when: the fix under design is a CACHE — caching a chatty
  pattern hides it and adds invalidation debt; the pattern gets fixed
  here first, and genuine cache candidates route to
  `caching-strategy-designer`.
- Do NOT use when: the chatter is service-to-service (one API call per
  item against another service) and the design question is the
  inter-service contract — batch endpoint design belongs to
  `api-event-architect` (external) or `architecture-designer`
  (internal boundaries); this skill still applies for the caller-side
  batching pattern.

## Inputs to Inspect

1. Per-request query evidence: the query log / APM span list for one
   symptomatic operation — statement shapes, counts, and total time vs
   per-query time. This is the primary artifact; obtain it before
   theorizing.
2. The data-access layer's semantics: which ORM/query layer, its lazy
   vs eager defaults, its batching facilities (preload/include,
   IN-key loaders, dataloader equivalents), and its per-request scope
   (where memoization can safely live).
3. The code path: the loop/serializer/template touching relations per
   item; resolver structure in GraphQL-style APIs (the per-field
   resolver is the N+1's natural habitat).
4. Result-set characteristics: typical and worst-case item counts,
   pagination state (or its absence), and whether the endpoint's
   latency scales with them.
5. Existing test infrastructure: where query-count assertions can live
   (the test layer that owns integration-level checks), and any
   existing counter/instrumentation hooks.
6. Write/consistency context for fix pricing: how hot the touched
   tables are, and whether denormalized read fields are maintainable.

## Workflow

Explicit human invocation selects this execution-capable skill; it does not
expand the allowed target or activate Task-Authorized Local Implementation
(TALI). TALI is a separate route requiring its own classification and
activation under the [skill-generation standard](../../../docs/skill-generation-standard.md#5-least-privilege--side-effects).
Use applicable existing user grants without repeated consent. Before a
state-changing command, confirm its actual files, environment and effects fit
those grants; if authority is absent, propose the operation and obtain it
before proceeding.

1. **Confirm the pattern with counts, not vibes.** For the symptomatic
   operation, produce the evidence table: distinct statement shapes ×
   repetitions × per-query time. The N+1 signature: one parent query +
   N near-identical child queries differing only in a key. Related
   signatures: repeated IDENTICAL queries (missing per-request
   memoization), serial awaits in a loop (wall-clock = N × round
   trip), and SELECT-everything over-fetch feeding two fields.
2. **Locate the injection site.** The loop, serializer, template, or
   per-field resolver touching a lazy relation per item. Name the file/
   path; the fix lands where the touch happens, not where the query
   runs. Static cues for review: relation access inside iteration,
   awaited calls in loops, missing `IN`-batching on keyed lookups.
3. **Choose the fix by pattern** (catalog in references):
   - *Known-ahead relations:* eager/preload declarations on the parent
     query — the default fix; price: over-eager loading drags unneeded
     data on paths that don't render the relation (scope eager-ness per
     use, not globally).
   - *Dynamic/conditional relations (resolvers, polymorphic):* batched
     loader — collect keys per request tick, one IN-key query,
     memoized per request and authenticated tenant/permission scope; price: loader infrastructure and
     per-request cache lifecycle.
   - *Aggregates per row:* one grouped aggregate query joined back,
     never per-row COUNT calls.
   - *Serial awaits:* batch the keys into one call where an interface
     exists; otherwise bounded-concurrency fan-out as the lesser fix,
     with the batch-endpoint need routed to the contract owner.
   - *Truly relational shapes:* a join producing the composed row set —
     price: row-width multiplication on one-to-many joins
     (deduplication cost); prefer two batched queries over exploding
     joins.
   - *Read-path denormalization:* a maintained count/summary column —
     price: consistency maintenance; only with an owner and an update
     rule.
   Over-fetch fixes ride along: project the fields the path uses.
3½. **Reject the cache reflex explicitly.** Caching the chatty result
   is not on the fix menu here: it hides the pattern at the price of
   invalidation debt, and the first cache miss replays the storm.
   After the pattern is fixed, a genuine staleness-tolerant candidate
   may route to `caching-strategy-designer` on its own merits.
4. **Verify by re-measuring the count when a fix is implemented.** Same operation, same data
   shape: statement count drops to the designed number (typically 2–3),
   wall-clock drops accordingly, and — the check that catches over-
   correction — the replacement queries' plans are sane
   (`query-plan-reader` if the new IN-key or join query is itself
   heavy at volume).
5. **Specify or install the regression guard according to task scope.** A query-count budget assertion in
   the integration layer for the fixed operation: "renders with ≤ K
   queries" (exact-count where stable, small budget where seeding
   varies). The budget documents intent — the next lazy touch fails a
   test instead of shipping. Wire the counter via the ORM's
   instrumentation hook; keep budgets on the endpoints that matter,
   not everywhere (assertion noise erodes the guard).
6. **Report** in the Output Format: evidence table, injection sites,
   fix per site with price, measured before/after counts when implementation
   and measurement occurred, otherwise an explicit unmeasured status and
   proposed guard location.

Pattern catalog with fix templates, counter-instrumentation options,
and query-budget assertion patterns:
[references/chatty-access-patterns.md](references/chatty-access-patterns.md).

## Output Format

```
N+1 / CHATTY ACCESS REPORT — <operation>
Evidence: <statement shapes × repetitions × per-query time; total vs per-query>
Signature: N+1 | repeated-identical | serial-await | over-fetch (combinations common)
Injection site(s): <file/path — loop, serializer, resolver, template>
Fix per site:
  <site>: <eager|batched-loader|grouped-aggregate|batch-call|join|denormalize>
  — price: <over-eagerness / loader lifecycle / row-width / consistency>
Cache reflex: rejected here — pattern fixed first (candidates → caching-strategy-designer after)
Verification: queries <before> → <after (designed number)>; wall-clock <before> → <after>;
              replacement-query plan sanity: <checked | routed to query-plan-reader>
Regression guard: <test file/layer — budget K, exact|bounded, counter hook>
Residual: <paths with the same pattern not yet fixed — listed, not implied done>
```

## Validation Checklist

- [ ] The pattern is evidenced by a count table from a real operation —
      not inferred from code reading alone.
- [ ] The injection site is named at file/path level; the fix lands at
      the touch point.
- [ ] The chosen fix matches the pattern class and states its price;
      no global eager-loading, no reflexive cache.
- [ ] One-to-many joins were checked for row-width explosion; batched
      two-query shapes preferred where they explode.
- [ ] When an authorized fix was run, before/after counts were measured and
      the after-count meets the design. Otherwise the proposed fix and guard
      are labeled unmeasured.
- [ ] When a replacement query ran, its plan was checked at realistic volume;
      otherwise the plan check is an explicit implementation task.
- [ ] When a guard was authorized and installed, its query-count assertion
      uses a real counter hook; otherwise the proposed guard is specified.
- [ ] Unfixed sibling paths with the same pattern are listed as
      residual.

## Gotchas

- Fast queries hide slow requests: per-query time of 2ms × 400 queries
  is 800ms of pure round-trips — dashboards ranking slow QUERIES never
  surface it. Count per request, not per statement.
- Global eager loading is the overcorrection: preloading every
  relation everywhere fixes one page and bloats twelve — eager-ness is
  scoped per use path.
- The batched IN-query can become the new problem: a 10,000-key IN
  list has its own plan pathology; chunk the keys and sanity-check the
  batched query's plan at worst-case volume.
- One-to-many joins multiply row width: joining a parent to 200
  children repeats the parent 200 times on the wire; two batched
  queries beat the exploding join.
- Per-request memoization leaking across requests: a loader cache
  scoped wrong (process-global) serves user A's rows to user B —
  in multi-tenant systems this is a data leak, not just staleness.
  Request lifetime and authenticated tenant/permission scope are both
  correctness boundaries, including in admin requests spanning tenants.
- GraphQL-style resolvers reinvent N+1 per field: each field resolver
  fetching independently is the architecture working as designed —
  batched loaders per entity type are the standing fix, not per-query
  patches.
- Exact-count assertions flake on seed drift: where fixture rows vary,
  assert a small budget (≤ K) instead of equality — the guard should
  catch a storm, not a seeding change.
- Pagination absence turns fixes moot: an unpaginated list of 50,000
  items is slow after every fix; pagination discipline (bounded page
  sizes) rides along with the pattern fix.

## Stop Conditions

- The skill was selected automatically rather than explicitly invoked by the
  human: do not execute it. An agent may recommend the named manual skill.
- A write, Git/network operation, database command or test side effect exceeds
  the applicable human grant: stop that operation and obtain the missing scope.
  Existing authorized operations do not need the same permission again.

- Asked to fix by caching ("just cache the page/result") → refuse the
  reflex: the pattern gets fixed first; caching a chatty pattern hides
  it and adds invalidation debt. Genuine cache candidates route to
  `caching-strategy-designer` after the count is sane.
- No per-request query evidence can be obtained (no logs, no
  instrumentation, no local reproduction) → stop and install the
  counter hook first; pattern fixes designed from code-reading alone
  misfire on framework internals.
- The evidence shows ONE heavy query, not many fast ones → route to
  `query-plan-reader`; this skill's fixes would restructure access
  that isn't the problem.
- The per-item calls are cross-service and the batch interface doesn't
  exist → design the caller-side batching AND route the batch-endpoint
  contract to its owner (`api-event-architect` / the owning team);
  do not invent another service's API unilaterally.
- Fixing the pattern requires schema change (denormalized counts,
  new keys) → the read-field design is this skill's output, but the
  migration itself follows `schema-evolution-planner` staging and the
  repo's change process — hand off, don't inline DDL.

## Supporting Files

- [references/chatty-access-patterns.md](references/chatty-access-patterns.md)
  — the pattern catalog with fix templates per ORM idiom class,
  counter-instrumentation options, IN-list chunking guidance, and
  query-budget assertion patterns (exact vs bounded).
- `evals/evals.json` — behavior cases including the resolver-storm edge
  and the cache-it-instead refusal.
- `evals/trigger-evals.json` — discrimination against
  `query-plan-reader` (the sibling seam), `profiling-methodology-designer`,
  and `caching-strategy-designer`.

Files in this skill

  • SKILL.md12.9 KB
  • evals/evals.json4.7 KB
  • evals/trigger-evals.json3.5 KB
  • references/chatty-access-patterns.md4.2 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.