typescript 88 lines · 3 tabs

Prisma: avoid N+1 with include/select

Shared by codesnips Jan 2026
3 tabs
import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

export async function getFeedNaive(limit = 20) {
  const posts = await prisma.post.findMany({
    orderBy: { createdAt: 'desc' },
    take: limit,
  });

  // N+1: one query per post for author, one per post for comments
  const feed = [];
  for (const post of posts) {
    const author = await prisma.user.findUnique({
      where: { id: post.authorId },
    });

    const comments = await prisma.comment.findMany({
      where: { postId: post.id },
      orderBy: { createdAt: 'asc' },
    });

    feed.push({ ...post, author, comments });
  }

  return feed;
}
3 files · typescript Explain with highlit

Prisma's fluent API makes it easy to accidentally trigger the classic N+1 problem: one query fetches a list of parents, then a loop lazily loads each parent's children one row at a time. This snippet shows the naive version, the fixed version using include/select, and a small service that keeps the eager-loading shape typed and reusable.

In feed.naive.ts, getFeedNaive fetches every post with prisma.post.findMany, then iterates and calls prisma.user.findUnique and prisma.comment.findMany per post. For a page of 20 posts this fires 1 + 20 + 20 queries. The code compiles and works in development, which is exactly why the pattern survives into production — the cost only shows up under real row counts and network latency to the database. Each round trip pays connection and planning overhead, so latency grows linearly with the result set.

In feed.eager.ts, getFeed collapses everything into a single query. The include on author and comments tells Prisma to emit one SQL statement with JOINs (or a small bounded set of batched queries) rather than per-row lookups. Nested select narrows the columns so the wire payload stays small, and take inside the nested comments relation caps how many children each parent pulls — important because an unbounded nested include can turn one N+1 into one giant cartesian result. The orderBy and outer take/skip keep pagination deterministic.

The key trade-off is between round trips and payload width. include fetches full related records; select is preferred when only a few fields are needed, avoiding over-fetching large columns like body. Deeply nested includes can also produce wide, repeated parent data, so at some depth a separate batched query keyed by id (a DataLoader-style approach) beats a single mega-join.

In PostService.ts, the postWithAuthor argument is captured with Prisma.validator and Prisma.PostGetPayload, so the eager shape is defined once and the return type is inferred exactly. This prevents the include shape and the consuming code from drifting apart, and makes it a compile error to read a relation that was never loaded — the type system enforces that eager loading and access stay in sync.


Related snips

Share this code

Here's the card — post it anywhere.

Prisma: avoid N+1 with include/select — share card
Link copied