1
+N
SQL
SLOW
N+1 Query
orm • join • batch

N+1 Query in Prisma, TypeORM, and Sequelize: How to Detect and Fix It

N+1 query causes no errors but makes APIs slow as data grows — ORM fetches a list with 1 query then fires N more for each item. How to catch it with SQL logging, count queries per request, and fix with eager loading, batching, or DataLoader.

11 min read18/06/2026

The problem

The app runs fine in development, but once it hits production with real data, an API endpoint suddenly takes 3–10 seconds. No errors, no exceptions — just slow. Looking at the database log shows hundreds of tiny identical queries firing in a loop.

That's N+1.


What N+1 query is

N+1 happens when code fetches a list (1 query), then fires one more query per item in that list — totaling 1 + N queries instead of just 1–2.

Classic example with Prisma (TypeScript):

// Fetch 100 posts
const posts = await prisma.post.findMany({ take: 100 });

// For each post, fetch the author → 100 separate queries
for (const post of posts) {
  const author = await prisma.user.findUnique({
    where: { id: post.authorId },
  });
  console.log(post.title, author.name);
}

Result: 101 queries for 100 posts. With 500 posts → 501 queries.

With TypeORM:

const posts = await postRepository.find({ take: 100 });

for (const post of posts) {
  // TypeORM lazy loading → separate query for each post
  const author = await post.author;
  console.log(post.title, author.name);
}

With Sequelize:

const posts = await Post.findAll({ limit: 100 });

for (const post of posts) {
  const author = await post.getAuthor(); // N queries
}

This pattern appears in every ORM, every language. Common places it hides:

  • Serializers / transformers when formatting responses
  • Service layer fetching related data
  • Templates rendering nested objects

Why it's dangerous

10 items 100 items 1,000 items
Queries fired 11 101 1,001
Estimated latency ~50ms ~500ms ~5s

N+1 throws no errors — the app still returns correct results. In development with a small dataset nobody notices. On production with real data it's slow, and gets slower as traffic grows.

Each extra query adds a round-trip to the database (~1–5ms locally, ~10–50ms if the database is on a separate server). 1,000 queries × 10ms = 10 seconds.


How to detect it

1. Log all SQL queries

Prisma — enable query logging:

const prisma = new PrismaClient({
  log: ["query", "info", "warn", "error"],
});

Or use an event:

prisma.$on("query", (e) => {
  console.log(`[SQL] ${e.query} — ${e.duration}ms`);
});

When you see dozens of SELECT * FROM users WHERE id = ? lines with different id values back-to-back → there's an N+1.

TypeORM:

const dataSource = new DataSource({
  logging: ["query", "error"],
  // or logging: true to log everything
});

NestJS + TypeORM:

TypeOrmModule.forRoot({
  logging: process.env.NODE_ENV === "development",
})

2. Count queries per request

With Express/NestJS, use middleware to count queries within a single request:

let queryCount = 0;

prisma.$use(async (params, next) => {
  queryCount++;
  const result = await next(params);
  return result;
});

app.use((req, res, next) => {
  queryCount = 0;
  res.on("finish", () => {
    if (queryCount > 20) {
      console.warn(`[N+1 WARNING] ${req.path} — ${queryCount} queries`);
    }
  });
  next();
});

3. Assert query count in tests

const queries: string[] = [];
prisma.$on("query", (e) => queries.push(e.query));

await getPostsWithAuthors(); // function under test

expect(queries.length).toBeLessThanOrEqual(3); // must not exceed 3 queries

4. APM tools

Datadog, New Relic, and Sentry Performance can group slow traces and show DB call counts per endpoint. A trace with 100+ small identical DB spans is a clear N+1 signal.


How to fix it

1. Eager loading (most common fix)

Instead of lazy-loading each item, load all related data in a single query.

Prisma — use include:

// Before: 1 + N queries
const posts = await prisma.post.findMany({ take: 100 });
for (const post of posts) {
  const author = await prisma.user.findUnique({ where: { id: post.authorId } });
}

// After: single query with JOIN
const posts = await prisma.post.findMany({
  take: 100,
  include: { author: true },
});
// posts[0].author.name — direct access, no extra query

TypeORM — use relations:

// Before
const posts = await postRepository.find({ take: 100 });

// After: JOIN in one query
const posts = await postRepository.find({
  take: 100,
  relations: ["author"],
});

TypeORM QueryBuilder:

const posts = await postRepository
  .createQueryBuilder("post")
  .leftJoinAndSelect("post.author", "author")
  .take(100)
  .getMany();

Sequelize:

// Before
const posts = await Post.findAll({ limit: 100 });

// After
const posts = await Post.findAll({
  limit: 100,
  include: [{ model: User, as: "author" }],
});

2. Select only the fields you need

Eager loading can pull back more columns than needed. Use select to limit them:

const posts = await prisma.post.findMany({
  take: 100,
  select: {
    id: true,
    title: true,
    author: {
      select: { name: true, avatar: true },
    },
  },
});

3. Manual batch query

When the ORM doesn't support it or you need more control — query once for everything, then map in memory:

const posts = await prisma.post.findMany({ take: 100 });

// Collect all authorIds, query once
const authorIds = [...new Set(posts.map((p) => p.authorId))];
const authors = await prisma.user.findMany({
  where: { id: { in: authorIds } },
});

// Map in memory — O(n) instead of N queries
const authorMap = new Map(authors.map((a) => [a.id, a]));
const result = posts.map((post) => ({
  ...post,
  author: authorMap.get(post.authorId),
}));

2 queries instead of N+1, regardless of how large N gets.

4. DataLoader pattern (for GraphQL or nested resolvers)

N+1 is especially common in GraphQL because each field resolver runs independently. DataLoader fixes this by batching and deduplicating queries within the same event loop tick:

import DataLoader from "dataloader";

const userLoader = new DataLoader(async (ids: readonly number[]) => {
  const users = await prisma.user.findMany({
    where: { id: { in: [...ids] } },
  });
  // Must return in the same order as ids
  return ids.map((id) => users.find((u) => u.id === id) ?? null);
});

// Resolver for Post.author
const resolvers = {
  Post: {
    author: (post) => userLoader.load(post.authorId),
    // DataLoader batches all .load() calls in the same tick → 1 query
  },
};

Even with 100 Post resolvers running in parallel, DataLoader collects all authorId values and fires 1 query with WHERE id IN (...).


When eager loading causes problems

Eager loading fixes N+1 but can cause over-fetching — loading far more data than needed.

// 100 posts + each author + all of author's posts + all their comments...
const posts = await prisma.post.findMany({
  include: {
    author: {
      include: {
        posts: {
          include: {
            comments: true,
          },
        },
      },
    },
  },
});

Result: 1 query, but an extremely heavy one returning a massive payload — possibly slower than the original N+1.

The rule: only include or join the relations actually needed for that specific request. Combine with select to limit columns.


Summary

Situation Solution
ORM with eager loading support include / relations / leftJoin
Need more query control Manual batch + WHERE id IN (...)
GraphQL resolvers DataLoader
Serializer making extra queries Preload data before serializing
Not sure if N+1 exists Enable SQL logging, count queries per request

N+1 is easy to fix — the hard part is detection. Enable query logging in the dev environment, set a warning threshold when query count per request exceeds a reasonable number. Catching it while writing code is much easier than debugging it on production with real-world data.