No N+1 query patterns
Why it matters
N+1 queries are the most common performance failure in AI-generated API code. A posts listing endpoint that fetches 50 posts and then runs 50 individual user lookups executes 51 database round-trips instead of 1. At low traffic this is invisible; at 100 concurrent users, each triggering a 51-query path, the connection pool saturates and requests queue. The pattern emerges because AI coding tools write posts.map(p => getUser(p.userId)) idiomatically in JavaScript without recognizing the database implications. ISO 25010 time-behaviour captures the direct impact: response latency scales linearly with list size rather than being constant.
Severity rationale
Critical because N+1 patterns multiply database round-trips with list size, saturating connection pools under moderate load and causing cascading timeouts.
Remediation
Replace per-record queries inside loops with Prisma include, SQL JOIN, or batch WHERE id IN (...) lookups.
// BEFORE — N+1: one user query per post
const posts = await prisma.post.findMany()
const withAuthors = await Promise.all(
posts.map(p => prisma.user.findUnique({ where: { id: p.userId } }))
)
// AFTER — eager load: 2 queries total
const posts = await prisma.post.findMany({
select: {
id: true, title: true, createdAt: true,
author: { select: { id: true, name: true } }
}
})
-- AFTER — single JOIN instead of N lookups
SELECT p.id, p.title, u.name AS author_name
FROM posts p
JOIN users u ON u.id = p.user_id
WHERE p.status = 'published'
ORDER BY p.created_at DESC
LIMIT 50;
Detection
-
ID:
no-n-plus-one -
Severity:
critical -
What to look for: Read the application's data fetching code — API route handlers, server components, service layer files, and repository classes. Look for: (1) A
findMany/SELECT *call that retrieves a list of records, followed by a loop (.map,.forEach,for...of) that runs afindUnique/findFirst/SELECTper record. This is the classic N+1 pattern — 1 query for the list plus N queries for each related item. (2) In Prisma, look forincludeorselectto confirm related data is loaded eagerly. AfindManywith noincludeon a model that has relations, followed by accessing those relations in a loop, is an N+1. (3) In raw SQL, look forSELECTstatements inside loops. (4) In GraphQL resolvers, look for resolvers that query the database without a DataLoader. (5) In ORMs with lazy-loading (TypeORM, Sequelize), accessing relation properties triggers a query — check whether relations are eagerly loaded for bulk operations. -
Pass criteria: Related data is fetched using eager loading — Prisma
include/selectwith nested relations, SQLJOIN, Drizzlewith, or batch queries (WHERE id IN (...)). No database query executes inside a loop over a list of records. Count every data-fetching endpoint and verify at least 90% use eager loading or batch queries. -
Cross-reference: For connection pooling configuration that mitigates N+1 impact, see the
connection-poolingcheck in the Query Patterns category. -
Fail criteria: A query for a list of records is followed by per-record queries in a loop.
findUniqueorSELECTcalled N times for N records. TypeORM/Sequelize relation accessed in a loop without eager loading. -
Skip (N/A) when: Application has no data listing or collection endpoints — only single-record lookups (extremely rare).
-
Detail on fail: Specify where the N+1 pattern is found. Example:
"In src/app/api/posts/route.ts: posts fetched with findMany, then for each post, author fetched with findUnique(user.id) — N+1 pattern."or"In components/PostList.tsx: posts.map(post => getCommentCount(post.id)) executes one DB query per post.". -
Remediation: Use eager loading to fetch related data in one query:
// BEFORE — N+1 pattern (one query per post for the author) const posts = await prisma.post.findMany() const postsWithAuthors = await Promise.all( posts.map(post => prisma.user.findUnique({ where: { id: post.userId } })) ) // AFTER — eager loading with include (2 queries total: posts + batch of users) const posts = await prisma.post.findMany({ include: { author: true } }) // AFTER — with select for performance (only get needed fields) const posts = await prisma.post.findMany({ select: { id: true, title: true, createdAt: true, author: { select: { id: true, name: true, avatarUrl: true } } } })-- BEFORE — N+1 in raw SQL (separate query per post) -- for each postId: SELECT * FROM users WHERE id = $1 -- AFTER — JOIN to get everything in one query SELECT p.id, p.title, p.created_at, u.id as author_id, u.name as author_name FROM posts p JOIN users u ON u.id = p.user_id WHERE p.status = 'published' ORDER BY p.created_at DESC LIMIT 20;For cases where a join is impractical (e.g., fetching counts for many items), use batch queries:
// Batch lookup — 1 query total const userIds = posts.map(p => p.userId) const users = await prisma.user.findMany({ where: { id: { in: userIds } } }) const userMap = new Map(users.map(u => [u.id, u]))
External references
- iso-25010:2011 · performance-efficiency.time-behaviour — Time Behaviour
Taxons
History
- 2026-04-18·v1.0.0·Initial import from database-design-operations·automated