Dashboard

How to Fix N+1 Queries With an AI Coding Agent

A step-by-step guide to diagnosing and fixing N+1 query problems with an AI coding agent, including the exact broken and fixed code and the prompt that gets agents to find the loop instead of guessing.

Steve Jefferson
Steve Jefferson
Developer Advocate
3 September 20261 min read

Your AI-built app worked fine with ten rows of test data. Then real users showed up, a list page that used to load instantly now takes three seconds, and someone on your team says the word "N+1." Nine times out of ten they're right: one query fetches a list, then the code fires one more query per row to grab something related to it, like a post's author or its comments. Fixing n+1 queries with an ai coding agent works well once you stop asking it to "make this faster" and start asking it to count queries. Point the agent at real query logs or a request-scoped query counter, and it finds the loop causing the damage in minutes. Point it at the code alone and it guesses.

What an N+1 query problem actually looks like

Here's a typical example from a Node API built with Prisma and Express. It fetches a list of posts, then loops over them to pull each post's author and comments separately.

javascript
// GET /api/posts: fetches posts, then queries author + comments per post
app.get('/api/posts', async (req, res) => {
  const posts = await prisma.post.findMany({ take: 20 });

  const enriched = await Promise.all(
    posts.map(async (post) => {
      const author = await prisma.user.findUnique({
        where: { id: post.authorId },
      });
      const comments = await prisma.comment.findMany({
        where: { postId: post.id },
      });
      return { ...post, author, comments };
    })
  );

  res.json(enriched);
});

One query loads the 20 posts. Then the map() callback fires two more queries per post, one for the author and one for the comments. That's 1 + (20 x 2) = 41 queries for a page that should need one or two. Load 200 posts and you're at 401 queries. The code isn't wrong exactly, it's just paying a per-row tax that scales with your data instead of staying flat.

The symptom shows up in the query log, not the code

You can often spot an N+1 problem just by reading the loop, but the reliable way is to look at what actually hits the database. Turn on query logging, Prisma's log: ['query'] option, Django's connection.queries, or Sequelize's logging callback, and hit the endpoint once. A healthy request produces a small, fixed number of queries. A broken one produces a repeating pattern.

text
SELECT * FROM "Post" LIMIT 20
SELECT * FROM "User" WHERE "id" = $1
SELECT * FROM "Comment" WHERE "postId" = $1
SELECT * FROM "User" WHERE "id" = $1
SELECT * FROM "Comment" WHERE "postId" = $1
SELECT * FROM "User" WHERE "id" = $1
SELECT * FROM "Comment" WHERE "postId" = $1
... (repeats 20 times)

That repeating two-line block is the fingerprint of N+1. If the query count in your log scales with the number of rows returned instead of staying constant, you've found it. This is exactly the kind of evidence an ai coding agent database performance check should start from, not a guess about what "looks slow."

The fix is to ask for the related data up front instead of one row at a time. Prisma calls this eager loading with include, and it either joins the tables in a single query or batches the related lookups into one or two IN queries, depending on the relationLoadStrategy you choose, according to Prisma's own query optimization documentation.

javascript
// GET /api/posts: fixed, constant query count regardless of row count
app.get('/api/posts', async (req, res) => {
  const posts = await prisma.post.findMany({
    take: 20,
    include: {
      author: true,
      comments: true,
    },
  });

  res.json(posts);
});

Same result, but now Prisma fires two or three queries total, not one plus two per row. Fetch 20 posts or 2,000, the query count barely moves. Other ORMs solve the same problem the same way: Django's select_related and prefetch_related, and Sequelize's include option, both batch related lookups instead of issuing one per row. The pattern is identical even though the syntax differs.

Why "optimize this code" doesn't find N+1 problems

Tell an agent to "optimize this code" and it will usually rename a variable, add a try/catch, maybe suggest an index, and call it done. It has no evidence that the loop is the problem, so it pattern-matches on things that generically look like optimization. This is the same failure mode you'll hit if you ask broader AI coding tools to speed up a slow page without giving them a way to measure anything: without real signal, the agent produces plausible-looking changes that don't touch the actual bottleneck.

The prompt that actually finds N+1 queries

The fix is to make query counting part of the instructions, not an afterthought. Give the agent a way to see queries firing, and tell it explicitly to count them per request.

text
I suspect the GET /api/posts endpoint has an N+1 query problem.

1. Enable query logging for this request (Prisma: log: ['query'], or read the dev server's existing query log).
2. Hit the endpoint once with a request that returns 20 posts and count the exact number of SQL queries fired.
3. If the count scales with the number of posts returned (roughly 1 + 2N), find the loop, map(), or forEach() issuing a query per row and tell me the file and line number.
4. Rewrite the query using Prisma's include (or a single batched query with a WHERE ... IN clause) so the total query count stays constant no matter how many posts are returned.
5. Re-run the endpoint and show me the new query count and the new query log, so I can confirm it actually dropped.

That prompt works because it gives the agent three things a naive prompt doesn't: where to look (the query log), what to measure (queries per request), and what success looks like (a constant count). This is the gap between a vague ask and knowing how to prompt ai to optimize queries: give it a way to measure, and it stops guessing. Ask it to "optimize this code" and it has to invent all three. Ask it to count queries per request and reason from the log, and it can't fake the answer, either the count drops or it doesn't.

Confirm the fix actually holds

Don't take the agent's word for it. Re-run the query log yourself after the fix ships, and check two things: the query count for a small list (20 rows) and the query count for a larger one (200 rows). If both show roughly the same small number of queries, the fix held. If the count still climbs with row count, either the eager load missed a relation, or there's a second loop somewhere else in the response path, maybe a serializer computing something per row after the data is already fetched.

Why this matters beyond page speed

N+1 queries don't just make pages feel slow, they make pages expensive. Every extra round trip is a connection held open, a bit of compute on your database, and on managed Postgres or serverless database plans, often a metered read. An endpoint that fires 400 queries instead of 2 will show up directly on your hosting bill, which is one reason database performance work pays for itself faster than most people expect, something worth checking against how much it actually costs to run an ai built app once N+1 problems are fixed. Getting the schema right in the first place also matters, which is part of why picking a database that fits your access patterns is worth doing early rather than retrofitting later. And once you're comfortable asking an agent to reason from evidence instead of guessing, the same discipline carries over to schema changes: the same query-log-first approach applies when using an agent for database migrations too.

Frequently asked questions

What is an N+1 query problem in simple terms?

It's when your code runs one query to get a list of N items, then runs one more query per item to fetch something related to it, for a total of N+1 queries instead of one or two. The database work grows in a straight line with the size of the list, so it feels fine in a demo with five rows and falls apart with five thousand.

How do I know if my app has an N+1 query problem?

Turn on query logging for your ORM and hit the slow endpoint once. Count the queries. If the count scales with the number of rows in the response, roughly one query per item, or one query per item per relation, that's the signature. A flat, predictable query count regardless of list size means you're fine.

Can AI coding agents actually detect N+1 queries reliably?

Yes, but only when you give them query logs or a way to count queries per request. Agents reasoning from code alone tend to guess at patterns that look inefficient rather than confirming which loop actually fires extra queries. Point them at real logs and ask for a before-and-after query count, and the detection gets much more reliable.

Does eager loading always fix N+1 queries?

It fixes the common case, fetching a fixed set of related records for a list of parent rows. It won't help if the per-row work isn't a database query at all, like an API call inside a loop, or if the eager load only covers one relation while another loop still queries per row further down the function.

Is fixing N+1 queries worth it if the app isn't that slow yet?

Usually yes, because the cost scales with your data, not your current traffic. An N+1 query that adds 200ms today at 50 rows can add several seconds once a table has 5,000 rows, and each of those extra queries is metered spend on most managed database plans. It's cheaper to fix once than to keep paying for it as the table grows.

How did this land?

About the author

Steve Jefferson
Steve Jefferson

Developer Advocate

Steve builds something with Swarmz every week and writes up what worked, what broke, and what he'd do differently. Tutorials and hands-on guides are his lane.

Share

Get the next post in your inbox

One email a month. Product updates, engineering posts, and the best of Built with Swarmz.

I agree to receive emails about AI building tips and Swarmz product news. Unsubscribe any time.