YASD-TECH
YASD TECH
# Prisma

includeでリレーションをまとめて取得する(selectとの違い)

投稿日:2026/8/9

更新日:2026/8/9

ttitleImage

Prisma の include でリレーションをまとめて取得する

Prisma でユーザーを取ったら、そのユーザーの投稿も一緒に欲しい。そういうときに使うのが include
select とどっちを使うのか、なぜ同時に書けないのか、実際にどんな SQL が飛ぶのかまで整理する。

結論

  • include は「モデルの全カラム 指定したリレーション」を取る
  • select は「指定したものだけ」を取る。リレーションも select の中に書ける
  • 同じ階層で includeselect は併用できないPlease either choose select or include.
  • 一部カラム+リレーションが欲しいときは select の中にリレーションを書くのが正解
  • include は JOIN 一発ではなく、リレーションごとに別クエリが飛ぶ(N+1 ではない)

前提のスキーマ

ブログを題材にする。ユーザーが投稿を持ち、投稿がコメントを持つ、素直な1対多。

prisma
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 }

ts
const users = await prisma.user.findMany({
  include: {
    posts: true, // リレーション名に true を指定する
  },
});

返ってくる形。ユーザーの全カラムに加えて posts 配列がぶら下がる。

json
[
  {
    "id": 1,
    "name": "あおい",
    "posts": [
      { "id": 1, "title": "Prisma 入門", "authorId": 1 },
      { "id": 2, "title": "TypeScript の基本", "authorId": 1 }
    ]
  }
]

include を書かなければ posts はそもそも型に存在しない。

ts
const user = await prisma.user.findUniqueOrThrow({ where: { id: 1 } });
user.name;  // ✅ "あおい"
user.posts; // ❌ 型エラー

ネストして深いリレーションを取る

include の中にさらに include を書ける。投稿にぶら下がるコメントまで一気に取る場合。

ts
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 が書ける。

ts
const users = await prisma.user.findMany({
  include: {
    posts: {
      where: { published: true },        // 公開済みだけ
      orderBy: { createdAt: 'desc' },    // 新しい順
      take: 3,                           // 直近3件だけ
    },
  },
});

「ユーザー一覧に最新投稿3件だけ出したい」みたいな要件はこれで足りる。
自分でループを回して都度 findMany する必要はない。

件数だけ欲しいなら _count

投稿の中身は要らず件数だけ表示したい、というケースは _count を使う。実データを引っ張らない分だけ軽い。

ts
const users = await prisma.user.findMany({
  include: {
    _count: {
      select: { posts: true },
    },
  },
});

users[0]._count.posts; // → 2

includeselect の違い

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 }"]

⚠️ 同じ階層での併用はエラー

ts
// NG:実行時エラー "Please either choose select or include."
const user = await prisma.user.findUnique({
  where: { id: 1 },
  select: { name: true },
  include: { posts: true },
});

「名前だけ欲しい、でも投稿も欲しい」は select に寄せる。

ts
// OK
const user = await prisma.user.findUnique({
  where: { id: 1 },
  select: {
    name: true,  // id は書いていないので返らない
    posts: true, // リレーションは select の中に書ける
  },
});

ネストの中では混ぜられる

禁止されているのは「同じ階層に並べること」だけ。階層が変われば include の中に select を書いてよい。

ts
// OK:ユーザーは全カラム、投稿はタイトルだけ
const users = await prisma.user.findMany({
  include: {
    posts: {
      select: { title: true },
    },
  },
});

発行される SQL は JOIN 1本ではない

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 にクエリ引数と同じ形を渡す。

ts
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 と違ってスキーマ変更に取り残されない。


ハマりどころ

1対1・任意のリレーションは null になりうる

Post.author のような必須のリレーションは常に値が入るが、スキーマ側が ? の場合は
include しても null が返る。取れている前提で .name を触ると落ちる。

prisma
coverImageId Int?
coverImage   CoverImage? @relation(fields: [coverImageId], references: [id])
ts
const post = await prisma.post.findUniqueOrThrow({
  where: { id: 1 },
  include: { coverImage: true },
});

post.coverImage.url;  // ❌ null の可能性があるので型エラー
post.coverImage?.url; // ✅ オプショナルチェーンが要る

使わないリレーションを include しない

「とりあえず全部 include」は転送量とクエリ数をそのまま増やす。
一覧 API で本文やコメント全件まで引いていないか、レスポンスを一度目で見て確認する。

take はリレーション単位で効く

include: { posts: { take: 3 } } は「親1件あたり3件」であって「全体で3件」ではない。
findMany 側の take と混同しない。


参考

Index

  • Prisma の include でリレーションをまとめて取得する
  • 結論
  • 前提のスキーマ
  • 基本:include: { リレーション名: true }
  • ネストして深いリレーションを取る
  • include の中で絞り込む
  • 件数だけ欲しいなら _count
  • include と select の違い
  • ⚠️ 同じ階層での併用はエラー
  • ネストの中では混ぜられる
  • 発行される SQL は JOIN 1本ではない
  • 戻り値の型を取り出す
  • ハマりどころ
  • 1対1・任意のリレーションは null になりうる
  • 使わないリレーションを include しない
  • take はリレーション単位で効く
  • 参考