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_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 で宣言的に取得すれば、行ごとに追加クエリが飛びません。
// ❌ 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等で単一クエリ化します。ただし執筆時点では Preview(relationJoinsプレビューフラグが必要)で、将来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 は「引き算(既定から要らないものを抜く)」です。omit は Prisma 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 本体のチューニングと合わせれば、体感は大きく変わります。