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 }, // 本文テキスト等、不要な大容量カラムまで全件取得
});
  1. 不要カラムの読み込み事故: posts テーブルの content(長文本文)や created_at などの不要データまで全件展開され、Node.js メモリ(RAM)が急増して OOM クラッシュの原因になる
  2. ラウンドトリップ遅延: リレーション階層が深くなると DB へのクエリ回数が激増し、API レスポンス速度が数百ms遅延する

最適化手順

  1. 取得不要な大容量カラムを除外するため、include ではなく select を使用する
  2. schema.prisma または クエリレベルで relationLoadStrategy: 'join' を指定する
  3. Prisma のデバッグログ(log: ['query'])を有効化し、単一の SELECT ... FROM "User" LEFT JOIN ... にまとめられたか検証する