Prisma ORMで発生するN+1問題の検知とselect・relationJoinsによる最適化
Prisma Database Node.js TypeScript
結論
全カラムの include をやめ、select による絞り込みと relationLoadStrategy: 'join' を使用します。
// Prisma公式仕様:relationLoadStrategy: 'join' による単一クエリ化
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
// 単一の SQL (LATERAL JOIN / subquery) として1回の通信で一括取得
const usersWithPosts = await prisma.user.findMany({
relationLoadStrategy: 'join', // Prisma 5.x+ 公式仕様
select: {
id: true,
name: true,
posts: {
select: {
id: true,
title: true,
},
},
},
});
Prisma公式仕様:デフォルトの query 戦略と join 戦略の違い
Prisma公式ドキュメント(prisma.io/docs/concepts/components/prisma-client/relation-queries)の規定通り、デフォルトの Prisma は DB 側で JOIN を行わず、アプリケーション層で別々の SQL クエリを発行して結合する方式をとります。
| 戦略 | 発行される SQL | 特徴 |
|---|---|---|
query (デフォルト) | 主テーブル+リレーションテーブルの 複数 SQL クエリ | DBのインデックス負荷を分散するが、通信ラウンドトリップが増加 |
join (relationJoins) | 単一の SQL (LATERAL JOIN / JSON_ARRAYAGG) | DB通信を1回に集約。N+1問題を物理的に解消 |
実際に起こる障害:include: { posts: true } によるメモリ溢れとN+1通信事故
// ❌ 事故:全カラムを無差別取得し、複数クエリを連打するダメなコード
const users = await prisma.user.findMany({
include: { posts: true }, // 本文テキスト等、不要な大容量カラムまで全件取得
});
- 不要カラムの読み込み事故:
postsテーブルのcontent(長文本文)やcreated_atなどの不要データまで全件展開され、Node.js メモリ(RAM)が急増して OOM クラッシュの原因になる - ラウンドトリップ遅延: リレーション階層が深くなると DB へのクエリ回数が激増し、API レスポンス速度が数百ms遅延する
最適化手順
- 取得不要な大容量カラムを除外するため、
includeではなくselectを使用する schema.prismaまたは クエリレベルでrelationLoadStrategy: 'join'を指定する- Prisma のデバッグログ(
log: ['query'])を有効化し、単一のSELECT ... FROM "User" LEFT JOIN ...にまとめられたか検証する