メインコンテンツへスキップ
友田 陽大
Prisma ORM
Prisma
TypeScript
PostgreSQL
パフォーマンス
型安全

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)まで、公式に忠実な実コードと計測起点の最適化順で解説します。

公開日
読了時間
10分
著者
友田 陽大
シェア

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

この記事は、Prisma(v7)のパフォーマンスを本番品質に引き上げる実装ガイドです。Prisma 全体の運用はPrisma ORM 本番運用ガイド(v7)に、スキーマ側のインデックス設計はスキーマ設計ガイドにあります。本記事は「速くする実装テクニック」を、効く順に並べます。

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


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

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

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

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

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


1. N+1 を撲滅する(最大の効果)

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

// ❌ 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 オペレータも有効です。

// 別の手:関連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 等で単一クエリ化します。ただし執筆時点では PreviewrelationJoins プレビューフラグが必要)で、将来 join が既定になる予定です。本番採用はフラグと検証を前提に。いずれにせよ include/select を素直に使えば N+1 は回避できます。


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

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

2.1 select:必要な列だけ取る

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

2.2 omit(GA):既定から機密・巨大列を除外する

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

// 単発:このクエリだけ 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方式あり、データ規模で選びます

// オフセット:実装は単純だが、深いページほど遅くなる
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にやらせます。

// 集計:平均・合計などは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 のコンストラクタで設定します。

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 実装ガイド


6. キャッシュ:Accelerate の cacheStrategy(ttl/swr)

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

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 フラグが必要)。

-- 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;
import { topAuthors } from "./generated/prisma/sql";

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

TypedSQL が Preview のうちは、通常の $queryRaw(タグ付きテンプレートで必ずパラメータ化)でも安全に書けます。$queryRawUnsafe にユーザー入力を渡さない——この一線は生SQLでも不変です(→Prisma 本番運用ガイド §8)。

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


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

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

  • まず計測:遅いクエリを特定(Prismaログ/APM/PostgreSQL統計/Query Insights)
  • N+1を撲滅:ループ内クエリを include/select、またはバッチの in に置換
  • インデックス:検索条件・並び替え・FK列に @@index(→スキーマ設計
  • 取りすぎ防止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 本体のチューニングと合わせれば、体感は大きく変わります。

友田

友田 陽大

経済産業大臣賞 受賞プロダクト開発者。TypeScript + Python + AWS で、SaaS・業界DX・ 実用レベルの生成AI(RAG)を、要件定義からインフラ・運用まで一人で完遂します。

この記事で解説した技術の適用事例

金融リテラシー教育のサブスク学習プラットフォーム(as/any/enum禁止+NeverError+Zod境界で型を徹底した実績)

ケーススタディを見る