As33
@periodic/
arsenic
n_plus_one
🔴 Critical

Multiple queries detected where a single query should suffice

Data is fetched in a loop instead of a single batch query. This is the classic N+1 problem — 1 query to fetch N items, then N queries to fetch related data for each. Performance degrades linearly with dataset size.

Common Causes

  • Iterating a result set and querying related data per item
  • Missing ORM populate/include (.populate in Mongoose, include in Prisma)
  • No DataLoader pattern for nested resolvers
  • Lazy loading without eager loading configured

How to Fix

  1. 1.Use batch queries or joins
  2. 2.Use Mongoose .populate() or Prisma include
  3. 3.Implement DataLoader pattern
  4. 4.Cache frequently accessed related data

Scales with your data

With 100 users this fires 101 queries. With 10,000 users it fires 10,001. N+1 patterns are invisible in development and catastrophic in production.

Example

typescript
// BAD — N+1: 1 query for users, then N queries for posts
const users = await User.find();
for (const user of users) {
  user.posts = await Post.find({ userId: user._id }); // fires once per user
}

// GOOD — Mongoose: single query with populate
const users = await User.find().populate('posts');

// GOOD — Prisma: single query with include
const users = await prisma.user.findMany({
  include: { posts: true },
});

GraphQL resolvers

N+1 is especially common in GraphQL. Use DataLoader to batch and cache per-request.

typescript
import DataLoader from 'dataloader';

const postLoader = new DataLoader(async (userIds: readonly string[]) => {
  const posts = await Post.find({ userId: { $in: userIds } });
  return userIds.map(id => posts.filter(p => p.userId.toString() === id));
});

// In your resolver — batched automatically
const resolver = {
  User: {
    posts: (user) => postLoader.load(user._id.toString()),
  },
};