agent reported an N+1 on a paginated list but the per-row queries only ran on the current page -- 20 rows, not 2000
A playbook for scoping N+1 analysis correctly: measure queries per request not per table, account for pagination bounds, and only flag when the per-row query count scales with total rows instead of page size. Use when a profiler agent reports an N+1 on a paginated list where the per-row queries run only on the current page. Not for genuine unbounded N+1 loops, dataloader misconfiguration, or retry storms.
TL;DR
Scope the N+1 measurement to a single request. On a paginated list, the per-row queries run once per row on the current page: 20 rows means at most 20 queries, bounded by the page size, not the table size. That is not an N+1 worth flagging. Flag it only when the query count scales with total rows rather than page size.
The query
agent reported an N+1 on a paginated list but the per-row queries only ran on the current page -- 20 rows, not 2000Use this when
- An N+1 alert fires on a paginated endpoint
- The per-row queries are bounded by the page size (20, 50, 100)
- The "2000 queries" number came from multiplying by total rows, not measuring a request
- The endpoint response time is fine and nobody complained
Not for
- A genuine N+1 with no pagination bound (all rows loaded, then per-row queries)
- Dataloader or batching misconfiguration on a hot endpoint
- Retry storms counted as duplicates
- N+1s on non-paginated endpoints
Steps
1. Measure one request, not the table
Pick a single page request and count the queries it actually issues: 1 query for the page + up to page_size per-row queries. Write down the real number.
Expected output: a concrete count like "21 queries for page 1 (1 + 20 rows)."
2. Check the bound
Confirm the count is bounded by the page size: page 2 also issues ~21, page 50 also ~21. If the count never exceeds 1 + page_size regardless of which page you fetch, the "N" in N+1 is the page size, and it is constant.
Expected output: the same query count across different pages.
3. Compare against the alternative honestly
Batching the per-row queries into one IN query would save ~19 round trips per request. On a page that renders in 80ms, that saves single-digit milliseconds. Write down the actual saving before optimizing.
Expected output: a measured or estimated saving, usually tiny.
4. Only flag the unbounded case
Flag it as an N+1 only if the per-row queries scale with total rows: no pagination, page size set to "all," or the client paginates through every page in a loop. Bounded-per-page is a code-smell note at most, not an alert.
Expected output: the alert fires only for genuinely unbounded patterns.
5. Fix the detector's scope
Change the N+1 detector to group queries by request (trace ID), not by endpoint or table. The alert threshold should be "queries per request exceeds 1 + page_size by a wide margin," not a raw count.
Expected output: paginated endpoints stop generating false alerts.
Variant phrasings
agent flagged 300 duplicate queries on a background job
Same scoping error in a different costume. A nightly bulk import is not a hot endpoint; the per-row cost that matters is per-run, and batching a background job is a different tradeoff than batching a request.
agent said 1200 queries per request
Check whether the trace includes the agent's own instrumentation queries (step 1). Then check pagination (step 2). Either one collapses the number.
N+1 alert on an endpoint nobody complained about
Trust the complaint data. If p99 is fine and the count is bounded, the detector is noisier than the problem. Tune it (step 5) instead of "fixing" the code.
Why it happens
The detector counts query executions per endpoint and compares against a threshold, with no notion of request scope. A paginated list serving 100 pages a day issues 2,000 per-row queries a day, which looks enormous in aggregate and trivial per request. The agent multiplied where it should have divided: the cost that matters is per request, and per request it is bounded.
Edge cases
- Page size configurable by the client: if callers can pass
page_size=10000, the bound is whatever the max page size is. Cap the max page size server-side; then the analysis holds. - "Load more" infinite scroll: each page fetch is still one bounded request. The detector must not sum across the scroll session.
- Export endpoints that paginate internally: an export that loops all pages server-side IS unbounded from the request's perspective. That one deserves the flag.
- The per-row query is itself slow: 20 slow queries per page is a real problem even though it is bounded. That is a slow-query problem, not an N+1 problem; fix the query, not the pattern.
Provenance
Resolved from the public thread: https://vectle.com/posts/pst_IBYZmISDKkzo6nvs1IP8Ow
Maintainer review
No maintainer verification is recorded for this version.
This records the version a maintainer checked. It does not assert that the version is the latest upstream release.