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;
}
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
export async function getFeed(limit = 20, cursorSkip = 0) {
return prisma.post.findMany({
orderBy: { createdAt: 'desc' },
take: limit,
skip: cursorSkip,
include: {
author: {
select: { id: true, name: true, avatarUrl: true },
},
comments: {
take: 3,
orderBy: { createdAt: 'desc' },
select: {
id: true,
body: true,
author: { select: { id: true, name: true } },
},
},
_count: {
select: { comments: true, likes: true },
},
},
});
}
import { Prisma, PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
const postWithAuthor = Prisma.validator<Prisma.PostDefaultArgs>()({
include: {
author: { select: { id: true, name: true, avatarUrl: true } },
comments: {
take: 3,
orderBy: { createdAt: 'desc' },
select: { id: true, body: true },
},
_count: { select: { comments: true, likes: true } },
},
});
export type PostWithAuthor = Prisma.PostGetPayload<typeof postWithAuthor>;
export class PostService {
async page(limit: number, skip: number): Promise<PostWithAuthor[]> {
return prisma.post.findMany({
...postWithAuthor,
orderBy: { createdAt: 'desc' },
take: limit,
skip,
});
}
displayName(post: PostWithAuthor): string {
// author is guaranteed loaded by the shared include shape
return post.author.name ?? 'anonymous';
}
}
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
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
package dbutil
import (
"context"
"github.com/jackc/pgconn"
Retry Postgres serialization failures with bounded attempts
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.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
# 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
# BAD: N+1 query problem
@users = User.all
@users.each do |user|
puts user.posts.count # Fires query for each user!
end
ActiveRecord query optimization and N+1 prevention
Share this code
Here's the card — post it anywhere.