投稿日:2026/8/9
更新日:2026/8/9

Prisma でユーザーを取ったら、そのユーザーの投稿も一緒に欲しい。そういうときに使うのが include。select とどっちを使うのか、なぜ同時に書けないのか、実際にどんな SQL が飛ぶのかまで整理する。
include は「モデルの全カラム + 指定したリレーション」を取るselect は「指定したものだけ」を取る。リレーションも select の中に書けるinclude と select は併用できない(Please either choose select or include.)select の中にリレーションを書くのが正解include は JOIN 一発ではなく、リレーションごとに別クエリが飛ぶ(N+1 ではない)ブログを題材にする。ユーザーが投稿を持ち、投稿がコメントを持つ、素直な1対多。
model User {
id Int @id @default(autoincrement())
name String
posts Post[] // 仮想フィールド。DB には存在しない
}
model Post {
id Int @id @default(autoincrement())
title String
authorId Int
author User @relation(fields: [authorId], references: [id])
comments Comment[]
}
model Comment {
id Int @id @default(autoincrement())
body String
postId Int
post Post @relation(fields: [postId], references: [id])
}
erDiagram
User ||--o{ Post : "1人が複数の投稿"
Post ||--o{ Comment : "1投稿に複数のコメント"
User {
int id
string name
}
Post {
int id
string title
int authorId
}
Comment {
int id
string body
int postId
}
posts / author / comments は DB のカラムではなく Prisma 上の仮想フィールド。
この仮想フィールドを実際に埋めるのが include。詳しくは リレーション入門。
include: { リレーション名: true }const users = await prisma.user.findMany({
include: {
posts: true, // リレーション名に true を指定する
},
});
返ってくる形。ユーザーの全カラムに加えて posts 配列がぶら下がる。
[
{
"id": 1,
"name": "あおい",
"posts": [
{ "id": 1, "title": "Prisma 入門", "authorId": 1 },
{ "id": 2, "title": "TypeScript の基本", "authorId": 1 }
]
}
]
include を書かなければ posts はそもそも型に存在しない。
const user = await prisma.user.findUniqueOrThrow({ where: { id: 1 } });
user.name; // ✅ "あおい"
user.posts; // ❌ 型エラー
include の中にさらに include を書ける。投稿にぶら下がるコメントまで一気に取る場合。
const users = await prisma.user.findMany({
include: {
posts: {
include: {
comments: true, // Post に紐づく Comment も取得
},
},
},
});
users[0].posts[0].comments[0].body まで型が通る。
ネストを深くするほど発行クエリが増える。3階層以上になったら、本当にその画面で全部要るのか一度立ち止まる。
include の中で絞り込むtrue の代わりにオブジェクトを渡すと、リレーション側にも where / orderBy / take / skip が書ける。
const users = await prisma.user.findMany({
include: {
posts: {
where: { published: true }, // 公開済みだけ
orderBy: { createdAt: 'desc' }, // 新しい順
take: 3, // 直近3件だけ
},
},
});
「ユーザー一覧に最新投稿3件だけ出したい」みたいな要件はこれで足りる。
自分でループを回して都度 findMany する必要はない。
_count投稿の中身は要らず件数だけ表示したい、というケースは _count を使う。実データを引っ張らない分だけ軽い。
const users = await prisma.user.findMany({
include: {
_count: {
select: { posts: true },
},
},
});
users[0]._count.posts; // → 2
include と select の違いinclude |
select |
|
|---|---|---|
| モデル本体のカラム | 全部取れる | 書いたものだけ取れる |
| リレーション | 追加で取れる | select の中に書けば取れる |
| 併用 | 同じ階層で select と併用不可 |
同じ階層で include と併用不可 |
| 向いている場面 | とりあえず一式欲しい | レスポンスを絞りたい・機微な列を出したくない |
flowchart TD
A["モデル本体は全カラム欲しい?"] -->|Yes| B["include を使う"]
A -->|No| C["select を使う"]
B --> D["include: { posts: true }"]
C --> E["select の中にリレーションも書く<br/>select: { name: true, posts: true }"]
// NG:実行時エラー "Please either choose select or include."
const user = await prisma.user.findUnique({
where: { id: 1 },
select: { name: true },
include: { posts: true },
});
「名前だけ欲しい、でも投稿も欲しい」は select に寄せる。
// OK
const user = await prisma.user.findUnique({
where: { id: 1 },
select: {
name: true, // id は書いていないので返らない
posts: true, // リレーションは select の中に書ける
},
});
禁止されているのは「同じ階層に並べること」だけ。階層が変われば include の中に select を書いてよい。
// OK:ユーザーは全カラム、投稿はタイトルだけ
const users = await prisma.user.findMany({
include: {
posts: {
select: { title: true },
},
},
});
「include は JOIN のようなもの」と説明されることが多いが、結果の形が JOIN 相当なだけで、
Prisma はデフォルトでリレーションごとに別クエリを投げ、アプリ側でマージしている。
sequenceDiagram
participant App as アプリ
participant DB as データベース
App->>DB: SELECT * FROM users
DB-->>App: ユーザー3件
App->>DB: SELECT * FROM posts WHERE author_id IN (1,2,3)
DB-->>App: 投稿まとめて
App->>App: メモリ上で users に posts を結合
ここで大事なのは ユーザー件数に比例してクエリが増えるわけではないこと。IN (...) で一括取得しているので、これは N+1 ではない。増えるのは「リレーションの種類の数」。
単一クエリの JOIN にしたい場合は、プレビュー機能の relationJoins を有効にしてrelationLoadStrategy: 'join' を指定する。数字が合わないときはまず生成 SQL を見るのが早い。
include した結果を関数の戻り値型に書きたいとき、手で型を組む必要はない。Prisma.<Model>GetPayload にクエリ引数と同じ形を渡す。
import { Prisma } from '@prisma/client';
type UserWithPosts = Prisma.UserGetPayload<{
include: { posts: true };
}>;
function render(user: UserWithPosts) {
return user.posts.map((p) => p.title);
}
クエリ側を include: { posts: { include: { comments: true } } } に変えたら、
型定義側も同じ形にすれば追随する。手書きの interface と違ってスキーマ変更に取り残されない。
null になりうるPost.author のような必須のリレーションは常に値が入るが、スキーマ側が ? の場合はinclude しても null が返る。取れている前提で .name を触ると落ちる。
coverImageId Int?
coverImage CoverImage? @relation(fields: [coverImageId], references: [id])
const post = await prisma.post.findUniqueOrThrow({
where: { id: 1 },
include: { coverImage: true },
});
post.coverImage.url; // ❌ null の可能性があるので型エラー
post.coverImage?.url; // ✅ オプショナルチェーンが要る
「とりあえず全部 include」は転送量とクエリ数をそのまま増やす。
一覧 API で本文やコメント全件まで引いていないか、レスポンスを一度目で見て確認する。
take はリレーション単位で効くinclude: { posts: { take: 3 } } は「親1件あたり3件」であって「全体で3件」ではない。findMany 側の take と混同しない。