Quick Answer
The N+1 query problem occurs when code executes one database query to get a list, then runs a separate query for each item in that list. AI code generators like Cursor and Bolt routinely put database calls inside loops, turning a 1-query operation into 100+ queries. The fix: use JOINs, batch queries, or ORM eager loading.
What Is the N+1 Query Problem?
The N+1 query problem is one of the most common performance killers in web applications. It happens when your code runs 1 query to fetch a list of N items, then runs N additional queries - one per item - to fetch related data. Instead of 1-2 database calls, you're making 101.
According to Veracode's performance analysis, N+1 queries are present in 34% of web applications and are the #1 database performance issue in production. For AI-generated code, the problem is even more prevalent because AI tools generate the simplest working pattern - which is almost always a loop with a query inside.
In a Next.js or Node.js app hitting a PostgreSQL or Supabase database, an N+1 query on a page with 50 items can increase page load time from 50ms to 2-5 seconds. With 100 items, your server response time can spike to 10+ seconds - enough to crash your UX and tank your Lighthouse score.
Exactly How AI Code Creates N+1 Queries
Here's a realistic example. You ask Cursor or Bolt: "Show a list of blog posts with author names." The AI generates this:
// ❌ BAD - N+1 Query (1 query + N queries)
export async function getPostsWithAuthors() {
// Query 1: Get all posts
const posts = await db.query('SELECT * FROM posts');
// N Queries: Get author for EACH post
const postsWithAuthors = await Promise.all(
posts.map(async (post) => {
const author = await db.query(
'SELECT name, avatar FROM users WHERE id = $1',
[post.author_id]
);
return { ...post, author: author.rows[0] };
})
);
return postsWithAuthors;
}
If you have 50 posts, this runs 51 database queries. With 200 posts, it's 201 queries. Each query has network latency (typically 1-5ms within the same data center). That adds up to 200ms-1000ms of pure waiting - on every single page load.
This pattern is caught by VibeDoctor's PERF-003 check, which detects database queries inside loops, .map(), .forEach(), and for...of constructs.
The Fix: One Query Instead of N+1
The solution is to fetch all related data in a single query using a JOIN, or in two queries using an IN clause:
// ✅ GOOD - Single JOIN Query
export async function getPostsWithAuthors() {
const result = await db.query(`
SELECT posts.*, users.name AS author_name, users.avatar AS author_avatar
FROM posts
JOIN users ON users.id = posts.author_id
ORDER BY posts.created_at DESC
`);
return result.rows;
}
// ✅ GOOD - Two Queries (batch approach)
export async function getPostsWithAuthors() {
const posts = await db.query('SELECT * FROM posts');
const authorIds = [...new Set(posts.rows.map(p => p.author_id))];
const authors = await db.query(
'SELECT id, name, avatar FROM users WHERE id = ANY($1)',
[authorIds]
);
const authorMap = new Map(authors.rows.map(a => [a.id, a]));
return posts.rows.map(post => ({
...post,
author: authorMap.get(post.author_id)
}));
}
The JOIN approach makes 1 query. The batch approach makes 2 queries. Both are constant - they don't grow with the number of posts.
N+1 in Prisma, Drizzle, and Supabase
ORMs don't automatically prevent N+1 queries. Here's how each tool handles it:
| ORM/Tool | N+1 Prone Pattern | Safe Pattern |
|---|---|---|
| Prisma | Loop with prisma.user.findUnique() |
prisma.post.findMany({ include: { author: true } }) |
| Drizzle | Loop with db.select().from(users).where() |
Use .innerJoin() or batch with inArray() |
| Supabase | Loop with .from('users').select().eq('id', x) |
.from('posts').select('*, users(*)') |
| Sequelize | Loop with User.findByPk() |
Post.findAll({ include: User }) |
Prisma's include and Supabase's nested select syntax handle JOINs automatically. But AI tools often don't use these - they generate the loop pattern because it's more explicit and matches the step-by-step nature of AI code generation.
Real-World Performance Impact
To put this in perspective, here are typical response times for a page that displays 100 items with related data:
| Approach | Queries | Response Time |
|---|---|---|
| N+1 (loop queries) | 101 | 500ms–2000ms |
| Batch (IN clause) | 2 | 10ms–30ms |
| Single JOIN | 1 | 5ms–20ms |
That's a 50-100x performance improvement. For an e-commerce product listing, a blog index, or a dashboard - this is the difference between a fast app and one that feels broken. Google's research shows that 53% of mobile users abandon pages that take longer than 3 seconds to load.
How to Find N+1 Queries in Your Codebase
- Search for database calls inside loops: Look for
awaitinside.map(),.forEach(),for...of, andwhileloops. If the await hits a database, it's likely N+1. - Check your ORM usage: If you're using Prisma, make sure list endpoints use
includeorselectwith relations. If you're using Drizzle, look for JOINs orinArray(). - Monitor query counts: Add logging to your database client to count queries per request. If a single API call generates 20+ queries, investigate.
- Run automated scanning: Tools like VibeDoctor (vibedoctor.io) automatically detect database queries inside loops (PERF-003) and flag the exact file and line number. Free to sign up.
- Load test your endpoints: Use a tool like k6 or Artillery to hit your endpoints with realistic data volumes. N+1 problems become obvious under load.
FAQ
Does Prisma automatically prevent N+1 queries?
No. Prisma prevents N+1 only when you use include or nested select. If AI generates a loop that calls prisma.user.findUnique() inside .map(), you still have an N+1 problem. Prisma does batch some internal queries, but it can't optimize explicit loops.
Is Promise.all() with map() still an N+1?
Yes. Promise.all(items.map(async item => db.query(...))) runs N queries in parallel instead of sequentially, which is faster - but it still makes N separate database calls. A single JOIN or batch IN query is always better.
How do I detect N+1 queries in production?
Use database query logging or APM tools like Datadog, New Relic, or even a simple query counter middleware. If an endpoint shows >10 queries, it likely has an N+1 problem. VibeDoctor's PERF-003 check catches them before deployment.
Does this affect Next.js Server Components?
Yes. Server Components and Server Actions are still Node.js code. If you fetch a list in a Server Component and then loop through items making individual database calls, the N+1 still happens - it just happens on the server instead of the client.
What about GraphQL and N+1?
GraphQL has a well-known N+1 problem with nested resolvers. DataLoader is the standard fix - it batches and deduplicates database calls within a single request. AI tools rarely generate DataLoader setup.