🧘‍♀️

PrismaはN+1を勝手に最適化してくれる…ケースもある

2024/02/27に公開

PrismaはNode.js界隈ではよく使われるORMのひとつです。ORMを活用する際にはN+1にならないように気を使って開発をする必要があるのですが、Prismaが実行するSQLが想像と違ったので調べてみました。

結論から言うと、Prismaが提供する findUnique メソッドは、N+1になるケースを勝手に最適化してくれる機能があります。

N+1を最適化するケース

これは、ユーザの一覧を取得し、その後各ユーザの詳細データを取得する例です。普通はこんなコードを書きませんが、わかりやすくするためにシンプルにしています。

const users = await prisma.user.findMany();

users.map(async (user) => {
  const result = await prisma.user.findUnique({
    where: { id: user.id },
  });
});

僕はこのコードを見た時にN+1だ。と思ったのですが実際に実行するとN+1にはなりません。以下のようなSQLが発行されます。

SELECT `main`.`User`.`id` FROM `main`.`User` WHERE 1=1 LIMIT ? OFFSET ?

SELECT `main`.`User`.`id` FROM `main`.`User` WHERE
  `main`.`User`.`id` IN (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) LIMIT ? OFFSET ?

これはPrismaが findUnique を実行する前にまとめて処理できそうならまとめちゃうからです。Prismaのドキュメントにはサラッと書いてあるので見逃してしまいました。

The Prisma Client dataloader automatically batches findUnique queries that ✔ occur in the same tick and ✔ have the same where and include parameters.

(意訳) 同じwhere/include条件で、かつ同じイベントループ内で実行されるときには自動でバッチ処理しちゃうよ

Next.js AppRouterで、コンポーネントに必要なデータは自分自身で取得するように書いていくと末端のコンポーネント内部でPrismaでSQLを実行するケースがありますが、条件さえ満たせばN+1にならずにかけることが分かりました。

余談:最適化されないケース

以下のような使い方だと最適化されずN+1になります。

const users = await prisma.user.findMany();

users.map(async (user) => {
  // findUnique限定
  const result = await prisma.user.findFirst({
    where: { id: user.id },
  });
});
const users = await prisma.user.findMany();

// for...ofで直列実行のため同じイベントループにはならない
for (const user of users) {
  const result = await prisma.user.findUnique({
    where: { id: user.id },
  });
}
ムーザルちゃんねる

Discussion