Prisma N+1: the `include` you chose over `select`, and the query count that explains a 4-second endpoint
verbatim errorprisma:query SELECT * FROM "orders" WHERE "orders"."user_id" = $1
prisma:query SELECT * FROM "items" WHERE "items"."order_id" = $1
prisma:query SELECT * FROM "items" WHERE "items"."order_id" = $1
prisma:query SELECT * FROM "items" WHERE "items"."order_id" = $1
(... 487 more queries for one page render, 4.1s total)
Problem
An orders page that takes 4 seconds and, with log: [{ query: 'stdout' }], shows the same items query repeated once per order:
prisma:query SELECT * FROM "orders" WHERE "orders"."user_id" = $1
prisma:query SELECT * FROM "items" WHERE "items"."order_id" = $1
prisma:query SELECT * FROM "items" WHERE "items"."order_id" = $1
prisma:query SELECT * FROM "items" WHERE "items"."order_id" = $1
(... 487 more queries for one page render, 4.1s total)Root cause
The loop pattern — fetch a list, then query per row — or its sneakier cousin: an include on the parent query that omits a nested level, followed by component code that lazily fetches the missing level per item:
// parent query includes orders but NOT their items
const user = await prisma.user.findUnique({
where: { id: userId },
include: { orders: true }, // items missing on purpose "to keep it light"
});
// template then fetches per order:
for (const order of user.orders) {
order.items = await prisma.item.findMany({ where: { orderId: order.id } }); // N+1
}The trap is the reasoning in the comment: include: { orders: { include: { items: true } } } feels heavy because it returns everything, so people split it up "for performance" and create the N+1 they were avoiding. The real fix is one query shaped narrowly, not many queries.
fix preview — first 3 of 17 lines (js), truncated:
const user = await prisma.user.findUnique({
where: { id: userId },
select: {
… 14 more lines in the fix🔒 the fix — including 2 code blocks — is members-only. $1/mo unlocks everything.