跳转到主要内容
模块 10 · 第 4 课(共 4)课程中的40/57~12 min
模块课程(4/4)

N+1问题

在后端开发中,导致 API 性能灾难性降低的最常见错误之一是臭名昭著的“N + 1 查询”问题。

对于使用“ORM”(对象关系映射器:例如 Prisma 或 Sequelize)等库抽象从 Node.js 与数据库对话的开发人员来说,它通常是未知的。

灾难剖析

想象一个应用程序想要返回整个电子商务主页列表:所有产品以及每个产品所属的类别名称字符串。 从 Node.js 方面来看,没有经验的程序员会编写迭代循环:

  1. products = await db.query("SELECT * FROM products"); (一项大而笨重的操作1)
  2. 开发人员对找到的 100 个数组中的每个 product 进行迭代 (for..of)
  3. 在 Node.js 中的每一步中,它们都会等待迭代子查询 categoryName = await db.query("SELECT name FROM categories WHERE id = " + product.category_id); 并将其嵌入到结果中(N 次缓慢、微小查询的二次迭代!)

对 Postgres 守护进程的请求总数: 1 + 100 次本地迭代 = 101 Node.js 和数据库之间的微网络往返。 101 次往返会导致可怕的网络开销和糟糕的延迟。在实际规模下,N 可能不是 100,而是 10,000 行待探索的发票总数! API 在达到 5xx 超时之前不会返回。

真正的本机答案:在单个查询中加入或聚合

熟练地使用手工编写的 DBMS 抽象,而不是 Node 中的琐碎 ORM 命令,在一个无与伦比的查询中以毫秒的速度将执行融合在一起,解决了“N+1”的混乱:

SQL
SELECT
  p.id,
  p.name,
  c.name AS category_name
FROM products p
JOIN categories c ON p.category_id = c.id;

API/DB 往返总数: 1 个唯一查询,由 Postgres 自己的索引融合、数学优化,完全以 0 毫秒的额外网络延迟处理。

JSON 聚合替代方案

现代 Postgres 版本甚至允许您使用 json_agg 返回嵌套为 JSON 的整个复杂连接,从而允许您的 JavaScript 接收 products 数组,包括其格式化评论的子数组,而无需任何本地脚本粘合! (在高级路径和挑战中,您将学习 Postgres 的 JSONb 子句。)

轮到你了

锻炼#sql.m10.l4.e1
尝试:0加载中...

让我们解决一个真正的 N+1 问题。\n如果 Node 想要回顾今天早上发货的订单,它会运行一个全局 Select,然后迭代每个客户来查找他们的名字。\n通过在单个本地镜头中打印“customers”(使用原始文本别名“c”)和“orders”(别名“o”)之间的 JOIN 来解决这个问题。\n提取 2 个琐碎字段(强制使用正式别名前缀): “c.first_name”(来自客户)和“o.status”(来自待处理订单)。

正在加载编辑器...
显示提示

语法: SELECT c.first_name, o.status FROMcustomers c JOIN 订单 o ON c.id = o.customer_id;

3 次尝试后可用的解决方案

探索 GROUP BY 效率

锻炼#sql.m10.l4.e2
尝试:0加载中...

如果数据库内部未使用逻辑 Group-By 来获取每行和类别的总计数,Node.js 将无休止地调用数据库,直至变成 N+1。\n编写一个查询,从“customers”中选择“city”字段,并使用terminal关键字按名称唯一地聚合它们,并使用标准 Postgres 计数“COUNT(id)”。

正在加载编辑器...
显示提示

一个简单的:SELECT city, COUNT(id) FROMcustomers GROUP BY city;

3 次尝试后可用的解决方案