# Prisma パフォーマンス最適化ガイド：N+1の撲滅、select/omit、カーソルページング、接続プール、cacheStrategy、TypedSQLまで

> Prisma（v7）のパフォーマンスを本番品質に引き上げる実装ガイド。include/selectによるN+1回避とinオペレータ、selectとomit（GA）の取りすぎ防止、オフセットvsカーソルのページング、aggregate/groupBy/_countの集計、driver adapterでの接続プール設定、AccelerateのcacheStrategy（ttl/swr）、ホットパスのTypedSQL（Preview）まで、公式に忠実な実コードと計測起点の最適化順で解説します。

- 公開日: 2026-06-26
- 著者: 友田 陽大
- タグ: Prisma, TypeScript, PostgreSQL, パフォーマンス, 型安全
- URL: https://tomodahinata.com/blog/prisma-performance-optimization-n-plus-1-connection-pool-guide

## 要点

- 最適化は計測から。まず遅いクエリを特定し、効く順（N+1→インデックス→取りすぎ→接続→キャッシュ）に直す。推測で書き換えない
- N+1はinclude/selectで撲滅。一覧＋関連が行数分に膨れず1〜2クエリで済む。バッチ取得はwhere idのinオペレータも有効
- 取りすぎを止める。selectで必要列だけ、omit（GA）で機密・巨大列を既定から除外。グローバルomitでpasswordを構造的に漏らさない
- 深いページはオフセットが劣化する。大規模データはcursorページングへ。集計はaggregate/groupBy、関連件数は_countでDB側に寄せる
- 接続プールはv7ではdriver adapter側で設定。サーバーレスはAccelerateのcacheStrategy(ttl/swr)で枯渇とレイテンシを同時に緩和。ホットパスはTypedSQL(Preview)

---

ORM の「遅い」は、たいてい ORM のせいではありません。**N+1、取りすぎ、インデックス欠如、接続の使い方**——原因の大半は使い方にあります。そして良いニュースは、それらが**型安全を保ったまま、宣言的に直せる**ことです。Prisma v7 は Rustフリー化でクライアント自体も軽量・高速になりました（公式ベンチマークで最大3.4倍高速・バンドル約90%減）が、本当に効くのはアプリ側の最適化です。

この記事は、**Prisma（v7）のパフォーマンスを本番品質に引き上げる**実装ガイドです。Prisma 全体の運用は[Prisma ORM 本番運用ガイド（v7）](/blog/prisma-orm-production-guide-type-safe-database-v7-driver-adapters)に、スキーマ側のインデックス設計は[スキーマ設計ガイド](/blog/prisma-schema-data-modeling-relations-design-guide)にあります。本記事は「**速くする実装テクニック**」を、効く順に並べます。

> **この記事のルール**：API・挙動は **Prisma 公式ドキュメント（2026年6月時点、v7系）** に基づきます。一部に「Preview（プレビュー）」段階の機能（`relationLoadStrategy` / TypedSQL）が含まれるため明記します。接続プールの細かな既定値は版で変わるため、本番投入前に必ず[公式ドキュメント](https://www.prisma.io/docs/orm/prisma-client/queries/query-optimization-performance)で確認してください。

---

## 0. メンタルモデル：計測してから、効く順に直す

性能改善の鉄則は「**推測するな、計測せよ**」です。私は PostgreSQL の本番チューニングでも、まず `pg_stat_statements` と `EXPLAIN ANALYZE` で重いクエリを特定し、**効く順**に手を入れます。Prisma でも同じで、当てずっぽうにクエリを書き換える前に、どのクエリが何回・何ms かを掴みます（Prisma のログ、APMのDBスパン、PostgreSQL側の統計）。

直す順番もほぼ決まっています。

> **① N+1の撲滅 → ② インデックス → ③ 取りすぎ（select/omit）→ ④ 接続プール → ⑤ キャッシュ → ⑥ ホットパスの生SQL。**

上ほど「効果が大きく・コストが小さい」。この順で潰すのが最短です。

---

## 1. N+1 を撲滅する（最大の効果）

N+1 問題——一覧を1クエリで取った後、各行の関連を1件ずつ引いてクエリが行数分に膨れる現象——は、ORM性能事故の筆頭です。Prisma では関連を **`include` / `select`** で宣言的に取得すれば、行ごとに追加クエリが飛びません。

```ts
// ❌ N+1：ユーザー取得後、ループで投稿を引く（1 + N クエリ）
const users = await prisma.user.findMany();
for (const u of users) {
  u.posts = await prisma.post.findMany({ where: { authorId: u.id } }); // 行数分
}

// ✅ include：投稿をまとめて取得（公式いわく「2クエリ」で済む）
const usersWithPosts = await prisma.user.findMany({
  include: { posts: true },
});
```

公式ドキュメントは、`include` を使うと「**ユーザー取得とポスト取得の2クエリ**」で完結すると明言しています。手動ループのように行数に比例して増えることはありません。**手動でバッチ取得**したい場面では `in` オペレータも有効です。

```ts
// 別の手：関連IDをまとめて1クエリで取る
const userIds = users.map((u) => u.id);
const posts = await prisma.post.findMany({
  where: { authorId: { in: userIds } },
});
```

> **発展（Preview）**：取得時の関連ロード戦略を選ぶ `relationLoadStrategy: "join" | "query"` があり、`"join"` は DB の `LATERAL JOIN` 等で**単一クエリ化**します。ただし**執筆時点では Preview**（`relationJoins` プレビューフラグが必要）で、将来 `join` が既定になる予定です。本番採用はフラグと検証を前提に。いずれにせよ `include`/`select` を素直に使えば N+1 は回避できます。

---

## 2. 取りすぎを止める：select と omit

ネットワーク転送・メモリ・シリアライズのコストは、**取得する列とレコード数に比例**します。一覧で本文や巨大JSONまで取れば、それだけで遅くなります。

### 2.1 select：必要な列だけ取る

```ts
const list = await prisma.post.findMany({
  select: {
    id: true,
    title: true,
    author: { select: { name: true } }, // ネストでも必要分だけ
  },
});
```

### 2.2 omit（GA）：既定から機密・巨大列を除外する

`select` が「足し算（要るものを挙げる）」なのに対し、`omit` は「**引き算（既定から要らないものを抜く）**」です。`omit` は **Prisma 6.2.0 で GA**（v7 ではフラグ不要）です。

```ts
// 単発：このクエリだけ password を除外
const user = await prisma.user.findUnique({
  where: { id: 1 },
  omit: { password: true },
});

// グローバル：全クエリで常に password を除外（最も安全）
const prisma = new PrismaClient({
  adapter,
  omit: {
    user: { password: true }, // 既定で漏れなくする
  },
});
```

> **設計のコツ（セキュリティ）**：`passwordHash` のような機密列は**グローバル `omit`** で既定から外しておくのが安全側です。「`select` し忘れたら漏れる」ではなく「**明示的に取りに行かない限り出ない**」状態を作る。取りすぎ防止は、性能と機密最小化を同時に満たします。

---

## 3. ページング：深いページはカーソルへ

ページングには2方式あり、**データ規模で選びます**。

```ts
// オフセット：実装は単純だが、深いページほど遅くなる
const page = await prisma.post.findMany({ skip: 20, take: 10 });

// カーソル：大規模データで安定・高速
const firstPage = await prisma.post.findMany({
  take: 10,
  orderBy: { id: "asc" },
});
const last = firstPage.at(-1);
const nextPage = last
  ? await prisma.post.findMany({
      take: 10,
      skip: 1, // カーソル行自身を除外
      cursor: { id: last.id },
      orderBy: { id: "asc" }, // 安定した並び順が必須
    })
  : [];
```

公式の指針どおり、オフセットは「**オフセットが大きくなるほど高コスト**」になります。「ページ番号でジャンプしたい管理画面」にはオフセット、「無限スクロールやフィード、巨大データ」には**カーソル**——と使い分けます。カーソルは安定した `orderBy`（一意なキー）が前提です。

---

## 4. 集計は DB に寄せる：aggregate / groupBy / _count

「アプリで全件取ってから数える/合計する」のは最悪です。**集計はDBにやらせます。**

```ts
// 集計：平均・合計などはDB側で
const stats = await prisma.user.aggregate({
  _avg: { age: true },
  _count: true,
  where: { active: true },
});

// グループ集計：having で集計値にフィルタ
const byCountry = await prisma.user.groupBy({
  by: ["country"],
  _sum: { profileViews: true },
  having: { profileViews: { _avg: { gt: 100 } } },
});

// 関連の件数：_count で「投稿数」をDB側で数える（投稿本体は取らない）
const usersWithCounts = await prisma.user.findMany({
  select: {
    name: true,
    _count: { select: { posts: true } }, // フィルタ付きも可
  },
});
```

`_count` は「ユーザー一覧に投稿数だけ出したい」という頻出要件を、**投稿本体を取らずに**満たします。一覧で関連を丸ごと `include` して `.length` する、というアンチパターンを避けられます。`distinct` で重複排除もDB側で可能です。

---

## 5. 接続プール：v7 は driver adapter で設定する

接続管理は、特にサーバーレスで性能と安定性を左右します。**ここは v6 から大きく変わりました。**v6 では接続文字列のクエリパラメータ（`connection_limit` など）で設定しましたが、**v7 では driver adapter のコンストラクタで設定**します。

```ts
import { PrismaPg } from "@prisma/adapter-pg";

const adapter = new PrismaPg({
  connectionString: process.env.DATABASE_URL,
  // プールサイズ・タイムアウトは adapter（＝ドライバ）側のオプションで
  // 例（pg ドライバ）：max（プール上限）, connectionTimeoutMillis, idleTimeoutMillis
});

export const prisma = new PrismaClient({ adapter });
```

要点。

- **プールサイズ・タイムアウトは adapter 側**（`pg` なら `max` / `connectionTimeoutMillis` / `idleTimeoutMillis`）。旧来の `DATABASE_URL?connection_limit=...` は v7 では使いません。
- **プールサイズはDBの上限から逆算**する：`max_connections` を、稼働しうるインスタンス数 × プールサイズが超えないように設計します。サーバーレスでインスタンスが大量に立つ構成では、これが**接続枯渇の主因**です。
- 具体的な既定値は版・ドライバで動くため、**自環境で `adapter` のオプションを明示**し、負荷試験で詰めるのが安全です。

> **サーバーレスの定石**：多数の関数が直接DBを叩くなら、プーラ（PgBouncer）か、接続プール内蔵の **Prisma Postgres** / グローバルプールの **Prisma Accelerate** を前段に置きます（→ §6）。詳しくは[Next.js × Prisma 実装ガイド](/blog/nextjs-prisma-app-router-server-actions-production-guide)。

---

## 6. キャッシュ：Accelerate の cacheStrategy（ttl/swr）

読み取りが重い・同じ結果を何度も返す箇所は、**キャッシュでDBそのものを叩かない**のが最も効きます。Prisma Accelerate を使うと、クエリ単位で `cacheStrategy` を指定できます。

```ts
import { withAccelerate } from "@prisma/extension-accelerate";

const prisma = basePrisma.$extends(withAccelerate());

const posts = await prisma.post.findMany({
  where: { published: true },
  cacheStrategy: {
    ttl: 60, // 60秒は無条件にキャッシュを返す（DBを叩かない）
    swr: 60, // ttl切れ後60秒は古い値を返しつつ背後で再取得
    tags: ["published_posts"], // タグで対象を絞って無効化
  },
});
```

- **`ttl`（time-to-live）**：この秒数内のヒットはDBクエリなしでキャッシュを返します。
- **`swr`（stale-while-revalidate）**：`ttl` 切れ後この秒数内は、**古い値を即返しつつ裏で更新**。レイテンシを犠牲にせず鮮度を回復します。
- **`tags`**：関連するキャッシュをまとめて無効化できます。

> **整合性の線引き（重要）**：`cacheStrategy` は**キャッシュしてよい読み取り**にだけ付けます。残高・在庫・権限のような「常に最新が要る」読み取りには付けない。`ttl`/`swr` の値は「どれだけ古くてよいか」というビジネス判断です。

---

## 7. ホットパス：TypedSQL（Preview）で型安全な生SQL

ORM では表現しづらい複雑な集計やDB固有最適化が要るホットパスには、生SQLを使います。Prisma の **TypedSQL** は、`.sql` ファイルから**パラメータと結果行の両方に型が付いた**関数を生成します（**Preview** 機能、`typedSql` フラグが必要）。

```sql
-- prisma/sql/topAuthors.sql
-- @param {Int} $1:minPosts
SELECT u.id, u.name, COUNT(p.id) AS post_count
FROM "User" u
JOIN "Post" p ON p."authorId" = u.id
GROUP BY u.id
HAVING COUNT(p.id) >= $1
ORDER BY post_count DESC;
```

```ts
import { topAuthors } from "./generated/prisma/sql";

// 戻り値も引数も型付き。手書きの $queryRaw より安全
const rows = await prisma.$queryRawTyped(topAuthors(5));
```

TypedSQL が Preview のうちは、通常の `$queryRaw`（タグ付きテンプレートで**必ずパラメータ化**）でも安全に書けます。**`$queryRawUnsafe` にユーザー入力を渡さない**——この一線は生SQLでも不変です（→[Prisma 本番運用ガイド §8](/blog/prisma-orm-production-guide-type-safe-database-v7-driver-adapters)）。

> **可観測性**：Prisma Postgres には、遅いクエリを可視化する **Query Insights** が同梱されます（かつての Prisma Optimize は Query Insights に置き換えられました）。本番のスロークエリは「勘」ではなく、こうした計測で特定して §0 の順で潰します。

---

## 8. パフォーマンス最適化チェックリスト

効く順に並べた、本番前の確認項目です。

- [ ] まず**計測**：遅いクエリを特定（Prismaログ／APM／PostgreSQL統計／Query Insights）
- [ ] **N+1を撲滅**：ループ内クエリを `include`/`select`、またはバッチの `in` に置換
- [ ] **インデックス**：検索条件・並び替え・FK列に `@@index`（→[スキーマ設計](/blog/prisma-schema-data-modeling-relations-design-guide)）
- [ ] **取りすぎ防止**：`select` で必要列だけ、機密・巨大列はグローバル `omit`
- [ ] **ページング**：深いページはオフセットからカーソルへ
- [ ] **集計はDB側**：`aggregate`/`groupBy`/`_count`/`distinct` を使い、全件取得して数えない
- [ ] **接続プール**：v7 は adapter 側で設定。`max_connections` から逆算して枯渇を防止
- [ ] **キャッシュ**：キャッシュ可能な読み取りに `cacheStrategy(ttl/swr)`。最新必須データには付けない
- [ ] **ホットパス**：必要なら TypedSQL（Preview）。生SQLは必ずパラメータ化
- [ ] バルク操作（`createMany`/`updateMany`/`deleteMany`）でループ書き込みを置換

---

## まとめ

Prisma を速くする道筋は、ORM の魔法ではなく**規律**です。計測してから、N+1 → インデックス → 取りすぎ → 接続 → キャッシュ → 生SQL の順で潰す。`include`/`select` で N+1 を消し、`omit` で取りすぎと機密漏れを止め、カーソルで深いページを救い、集計はDBに寄せ、接続は adapter で管理し、キャッシュは整合性を見極めて使う——どれも型安全を保ったまま実行できます。

「**遅いと言われ続けているアプリのDB層を、計測起点で本番品質まで引き上げたい**」——スロークエリの特定からスキーマ・クエリ・接続・キャッシュの最適化まで、効く順に手を入れるお手伝いができます。PostgreSQL 本体のチューニングと合わせれば、体感は大きく変わります。
