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

typescript
export interface RetryOptions {
  retries: number;
  baseMs: number;
  maxMs: number;
  signal?: AbortSignal;
  onRetry?: (attempt: number, delay: number, err: unknown) => void;

Exponential backoff with jitter for retries

typescript reliability retry
by codesnips 2 tabs
go
package dbutil

import (
  "context"

  "github.com/jackc/pgconn"

Retry Postgres serialization failures with bounded attempts

go postgres transactions
by Leah Thompson 1 tab
ruby
require "csv"

class PeopleCsvStream
  include Enumerable

  HEADERS = %w[id full_name email signed_up_at plan].freeze

Resilient CSV Export as a Streamed Response

rails performance streaming
by codesnips 3 tabs
ruby
Rails.application.configure do
  config.after_initialize do
    Bullet.enable = true
    Bullet.alert = false
    Bullet.bullet_logger = true
    Bullet.console = true

N+1 query detection with Bullet gem

rails performance activerecord
by Alex Kumar 2 tabs
ruby
# Vulnerable: user input is concatenated directly into SQL.
email = params[:email]
password = params[:password]

sql = "SELECT * FROM users WHERE email = '#{email}' AND password_hash = '#{password}'"
user = ActiveRecord::Base.connection.execute(sql).first

SQL injection prevention with unsafe and safe query patterns

sql-injection owasp database
by Kai Nakamura 3 tabs
ruby
json.array! @posts do |post|
  json.cache! ['v1', post], expires_in: 1.hour do
    json.id post.id
    json.title post.title
    json.excerpt post.excerpt
    json.published_at post.published_at

Fragment caching for expensive JSON serialization

rails caching performance
by Alex Kumar 1 tab

Share this code

Here's the card — post it anywhere.

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