HIGH Postgres · MySQL · Prisma · Supabase

Queries with no index, and the N+1 that follows

What you'd see: Pages that were instant during development take seconds in production, and get slower every week.

§ 01 — What's actually happening

Two problems usually arrive together. Without an index, every filtered query reads the entire table — invisible at 50 rows, fatal at 50,000. And a loop that queries inside itself turns one page load into hundreds of round trips. Neither shows up in testing, because test data is small.

Why an AI tool writes it this way

Schema generation optimises for correctness, not access patterns — indexes are only obvious once you know which queries matter. And <code>for (const x of items) { await fetch(x) }</code> is the most readable way to express "get the details for each", so it is what gets written.

What it costs you

Timeouts under normal traffic, a database bill climbing faster than your userbase, and connection-pool exhaustion that takes the whole app down rather than one slow page. On serverless the cost lands directly on your invoice.

§ 02 — Before & after

Fine with ten users, unusable at five hundred

Illustrative code, written for this page — never a client's project.

AI-generated

  • Unindexed columns in every where clause
  • One query per row inside a loop (N+1)
  • Whole table pulled into memory, then filtered in JavaScript
// One query for the posts, then one more per post. 51 round trips. const posts = await db.post.findMany({ where: { published: true } }); for (const post of posts) { post.author = await db.user.findUnique({ where: { id: post.authorId } }); post.comments = await db.comment.findMany({ where: { postId: post.id } }); } // And elsewhere: the whole table, filtered in the browser const all = await db.order.findMany(); const mine = all.filter(o => o.userId === user.id);

Human-reviewed

  • Indexes on the columns actually filtered and sorted
  • One query with a join or include
  • Filtering, sorting and pagination done by the database
// One query, joined by the database const posts = await db.post.findMany({ where: { published: true }, include: { author: { select: { id: true, name: true } }, comments: { take: 3, orderBy: { createdAt: 'desc' } } }, orderBy: { createdAt: 'desc' }, take: 20, skip: page * 20 }); // schema.prisma — index what you filter and sort on model Post { id String @id @default(cuid()) authorId String published Boolean createdAt DateTime @default(now()) @@index([published, createdAt]) @@index([authorId]) }

§ 03 — Check your own

How to tell in two minutes

You don't need us to run these. If any of them come back the wrong way, you have this problem.

  1. Turn on query logging and load your busiest page. If you see the same query repeated with different IDs, that is an N+1.
  2. Run EXPLAIN ANALYZE on your slowest query. Seq Scan on a large table means no usable index.
  3. Index every column you filter, sort or join on — foreign keys especially. Postgres does not index them automatically.
  4. Copy production-sized data into a staging database before you believe any performance result. 50 rows proves nothing.

Found it in your project? Fixing this one properly usually takes an engineer under an hour. Finding the other nine takes longer — which is what the free health check is for.

Get my free health check

Find out where you actually stand.

Free. No obligation. Reviewed by experienced engineers.

Get my free health check