From d5de65fdb1802cdf498d65d93397f290813c377c Mon Sep 17 00:00:00 2001 From: muqiuhan Date: Tue, 9 Sep 2025 06:17:00 +0000 Subject: deploy: 9665097f0fa0dae9f92123fac54d26c0818758a5 --- .../index.html" | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) (limited to '2025/04/02') diff --git "a/2025/04/02/N-1-selects-problem-\344\270\216-Prisma-ORM/index.html" "b/2025/04/02/N-1-selects-problem-\344\270\216-Prisma-ORM/index.html" index d8936873..819f6d02 100644 --- "a/2025/04/02/N-1-selects-problem-\344\270\216-Prisma-ORM/index.html" +++ "b/2025/04/02/N-1-selects-problem-\344\270\216-Prisma-ORM/index.html" @@ -198,10 +198,12 @@

现在,需要获取前 10 个用户以及他们各自的所有帖子。

一种有问题的 ORM 实现(或不当的使用方式)可能会这样执行:

    -
  1. 第一次查询 (The “1”): 获取前 10 个用户。
    1
    SELECT * FROM User LIMIT 10;
  2. -
  3. 接下来的 N (=10) 次查询 (The “N”): 对于上一步获取到的每一个用户,单独执行一次查询来获取该用户的帖子。
    1
    2
    3
    4
    5
    6
    7
    8
    -- 用户 1
    SELECT * FROM Post WHERE authorId = 1;
    -- 用户 2
    SELECT * FROM Post WHERE authorId = 2;
    -- 用户 3
    SELECT * FROM Post WHERE authorId = 3;
    -- ... 直到 用户 10
    SELECT * FROM Post WHERE authorId = 10;
  4. +
  5. 第一次查询 (The “1”): 获取前 10 个用户。
    1
    SELECT * FROM User LIMIT 10;
    +
  6. +
  7. 接下来的 N (=10) 次查询 (The “N”): 对于上一步获取到的每一个用户,单独执行一次查询来获取该用户的帖子。
    1
    2
    3
    4
    5
    6
    7
    8
    -- 用户 1
    SELECT * FROM Post WHERE authorId = 1;
    -- 用户 2
    SELECT * FROM Post WHERE authorId = 2;
    -- 用户 3
    SELECT * FROM Post WHERE authorId = 3;
    -- ... 直到 用户 10
    SELECT * FROM Post WHERE authorId = 10;
    +
-

在这个场景下,总共执行了 1 + 10 = 11 次数据库查询。如果 N 的值很大(比如获取 1000 个用户),就会产生 1001 次查询,这对数据库造成巨大的、不必要的压力,并显著增加应用程序的响应时间。每一次数据库交互都有网络延迟和数据库处理的开销,N+1 次查询会将这些开销放大 N 倍。

+

在这个场景下,总共执行了 1 + 10 = 11 次数据库查询。如果 N 的值很大(比如获取 1000 个用户),就会产生 1001 次查询,这对数据库造成巨大的、不必要的压力,并显著增加应用程序的响应时间。每一次数据库交互都有网络延迟和数据库处理的开销,N+1 次查询会将这些开销放大 N 倍。

N+1 问题通常源于 ORM 处理关联数据的方式,特别是与“懒加载”(Lazy Loading)相关的策略。懒加载是指只有在显式访问关联属性时,ORM 才会去数据库加载这些数据。虽然这在某些情况下可以避免加载不需要的数据,但如果在循环中访问关联属性,就很容易触发 N+1 问题。

然而,问题的根源在于没有有效地预先加载(或批量加载)所需的关联数据。即使不使用严格意义上的懒加载,如果 ORM 在处理关联查询时不够智能,采用了逐个获取关联对象的策略,同样会产生 N+1 查询。

在 Prisma 出现之前或在其他 ORM 中,解决 N+1 问题常见的方法包括:

@@ -212,14 +214,16 @@

Prisma ORM 在设计上就考虑了 N+1 问题,并提供了一种既方便开发者又高效的解决方案。当使用 Prisma Client 查询数据并需要包含关联模型时,Prisma 会自动优化查询,避免产生 N+1 查询,主要通过关系查询(Relation Queries)中的 include 选项或嵌套读取(nested reads)来实现这一点:

假设想获取所有用户及其发布的帖子,使用 Prisma Client,可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { PrismaClient } from '@prisma/client'

const prisma = new PrismaClient()

async function getUsersWithPosts() {
const usersWithPosts = await prisma.user.findMany({
include: {
posts: true, // 指示 Prisma 加载关联的 posts
},
})
// usersWithPosts 包含了用户列表,每个用户对象中都有一个 posts 数组
console.log(usersWithPosts)
}

getUsersWithPosts()
.catch((e) => {
throw e
})
.finally(async () => {
await prisma.$disconnect()
})
-

当执行上述查询时,Prisma 不会 生成 N+1 个 SQL 查询。而是首先会分析请求,并将其转化为数量非常有限的高效 SQL 查询。对于上面这个一对多关系的 include 查询,Prisma 通常会执行以下两步(类似于批量加载策略):

    -
  1. 查询父模型: 获取所有 User 记录。
    1
    SELECT "public"."User"."id", "public"."User"."name", /* ... other user fields */ FROM "public"."User" WHERE 1=1
  2. -
  3. 查询关联的子模型: 使用上一步获取到的所有用户 id,通过 WHERE IN (...) 子句一次性查询所有相关的 Post 记录。
    1
    SELECT "public"."Post"."id", "public"."Post"."title", "public"."Post"."authorId", /* ... other post fields */ FROM "public"."Post" WHERE "public"."Post"."authorId" IN ($1, $2, $3, ...) /* 这里的 $1, $2, ... 是第一步查到的用户 ID 列表 */
  4. +
  5. 查询父模型: 获取所有 User 记录。
    1
    SELECT "public"."User"."id", "public"."User"."name", /* ... other user fields */ FROM "public"."User" WHERE 1=1
    +
  6. +
  7. 查询关联的子模型: 使用上一步获取到的所有用户 id,通过 WHERE IN (...) 子句一次性查询所有相关的 Post 记录。
    1
    SELECT "public"."Post"."id", "public"."Post"."title", "public"."Post"."authorId", /* ... other post fields */ FROM "public"."Post" WHERE "public"."Post"."authorId" IN ($1, $2, $3, ...) /* 这里的 $1, $2, ... 是第一步查到的用户 ID 列表 */
    +

Prisma Client 在内存中将这两次查询的结果高效地组合起来,最终返回嵌套的、符合 TypeScript 类型的数据。

-

Refs.