Prisma relation loading: separate queries, not joins
Prisma Client loads related records with separate queries, not SQL JOINs. Learn when to use include versus select and how to spot extra round-trips with query logging.
01 Jun 2026, 21:12 UTC

The problem: one endpoint, more queries than you expect
You add include: { author: true } to a Prisma query, expecting the database to return posts and authors in a single result set. Instead, your logs show Prisma running one query for posts, then another for authors. If you nest include three levels deep, you can end up with several sequential round-trips before the response is sent.
This happens because Prisma Client does not use SQL JOIN to load relations. It issues separate queries per relation level and assembles the nested objects in the application layer. That design avoids the classic N+1 problem where every row triggers its own query, but it trades join complexity for extra database round-trips.
include loads everything; select lets you choose
include returns every column on the related model. If your User table has a long bio or a large JSON column, those bytes travel with every post even when your API only needs the author's name.
select requests only the fields you specify. It also works on relation fields, so you can load an author without loading their entire record.
Worked example: posts with author names only
Assume this schema:
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
bio String? @db.Text
posts Post[]
}
model Post {
id Int @id @default(autoincrement())
title String
content String?
authorId Int?
author User? @relation(fields: [authorId], references: [id])
}
Run this in your project root after editing schema.prisma:
npx prisma generate
Required permissions: read/write access to the project directory and node_modules. No database privileges are needed for generation, but the generated client needs a valid DATABASE_URL at runtime.
Create the client with query logging so you can see exactly what Prisma sends:
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient({
log: ['query'],
});
Place this file in your application entry point or a dedicated database module. The log: ['query'] option prints each SQL statement to the console. In production, keep this off or gate it behind an environment variable because it logs every statement and can expose parameter values.
Compare the two approaches:
// include loads the full User record, including bio
const postsWithFullAuthor = await prisma.post.findMany({
include: {
author: true,
},
take: 20,
});
// select loads only id and name on the related User
const postsWithAuthorName = await prisma.post.findMany({
select: {
id: true,
title: true,
author: {
select: {
id: true,
name: true,
},
},
},
take: 20,
});
Expected diagnostic check: with log: ['query'] enabled, both examples should print at least two SQL statements—one for posts and one for users. The select version should return a smaller JSON payload because bio is omitted. Verify locally by comparing JSON.stringify(postsWithFullAuthor).length versus JSON.stringify(postsWithAuthorName).length.
Limitation: nested includes create sequential round-trips
Prisma avoids classic N+1 by batching related IDs into a single query per level. However, deeply nested include still means multiple sequential queries. If you include post.author.team.members, Prisma may issue separate queries for posts, users, teams, and team members before returning the result. Latency adds up, especially over a managed database connection with network round-trips.
Prisma also does not support arbitrary SQL joins across unrelated tables. For reporting queries that span many relations, consider a raw query, a database view, or a dedicated analytics tool instead of forcing Prisma's relation loader to do work it was not designed for.
Practical closing
- Use
includewhen you genuinely need most fields on a related record and the nesting is shallow. - Use
selecton relations when you only need a few fields, especially if related tables contain large text or JSON columns. - Enable
log: ['query']in staging or local development to confirm how many statements a single endpoint triggers. - Run
npx prisma generateafter everyschema.prismachange so relation fields exist on the generated client. - If a nested include produces more than two or three sequential queries, split the fetch into multiple client calls or replace it with a raw query.
Prisma's separate-query approach is predictable, but it is not free. Check your logs, trim your selections, and match the loading strategy to the shape of the response your API actually needs.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.