← 中文资源

简体中文版本

为每个请求设置查询预算

统计单次请求执行的 SQL 条数,在测试中设置上限,并导出计数,让 N+1 回归在用户感知之前暴露。

阅读英文原文

概述

一个接口在 10 行数据时很快,在 500 行时却可能崩溃,因为查询条数随数据量线性增长而不是保持常量,这正是 N+1 模式。只用一两条测试数据的单元测试完全看不到它。查询预算把这个问题变成可测试的硬性属性:无论返回多少行,一个请求最多执行 N 条语句。

步骤

  1. 为连接池加计数包装,测试通过它统计语句条数:
export function countedPool(pool) {
  let executed = 0;
  const query = pool.query.bind(pool);
  pool.query = (...args) => { executed += 1; return query(...args); };
  return { pool, reset: () => { executed = 0; }, count: () => executed };
}
  1. 分别用 10 行与 50 行数据测量。健康的列表接口在预加载后稳定在 2 到 3 条;存在 N+1 时会增长到 50 条以上。
  2. 把预算写成断言,并在失败信息中带上实际条数与数据规模:
assert.ok(counted.count() <= 5, `query budget exceeded: ${counted.count()}`);
  1. 在生产请求中通过 AsyncLocalStorage 记录每个请求的语句条数并写入日志字段,便于仪表盘与慢请求排查。

验证

修复前 50 行订单应产生约 53 条语句,加上 include 或批量查询后应降为 3 条,测试通过。故意移除预加载,测试应立即失败并打印 “query budget exceeded: 53”。随后查看请求日志,确认 dbQueries 字段与测试计数一致。

注意事项

  • 语句条数不等于开销,十条廉价查询可能优于一条昂贵连接,预算应与延迟、执行计划一起看。
  • 只有经过被包装连接池的查询会被统计,直接新建 Client 或走外部缓存的路径不可见。
  • 预算过紧会阻碍正常功能,应按接口分别设定,并在评审中确认变更原因。
  • 导出类接口应按页或按批设置预算,否则单行查询的回归仍可能被总数掩盖。
  • 不要为了让 CI 变绿而随意上调预算,先弄清楚新增语句来自哪里。