全书目录

第十六篇:GraphQL 完全指南 —— 智能问询总台宇宙

第十一章:N+1 问题 —— 别让总台一个字段打一百通电话

1 分钟 344 字 第 315 / 962 个阅读单元

GraphQL 的字段式执行很优雅,但也容易产生著名的 N+1 问题

看一个查询:

graphql
query {
  tickets(status: OPEN) {
    id
    title
    device {
      id
      name
    }
  }
}

如果有 100 张工单,最天真的 resolver 写法可能会这样干:

  1. 先查一次工单列表
  2. 然后每张工单各自再查一次设备

也就是:

text
1 次查工单
+ 100 次查设备
= 101 次访问后端

这就是 N+1。

天真写法:

ts
const Ticket = {
  device: async (ticket, _args, context) => {
    return context.db.getDeviceById(ticket.deviceId);
  }
};

更合理的做法,是批量调度。常见方案就是 DataLoader 模式

ts
import DataLoader from "dataloader";

function createLoaders(db) {
  return {
    deviceById: new DataLoader(async (ids) => {
      const devices = await db.getDevicesByIds(ids);
      const map = new Map(devices.map((d) => [d.id, d]));
      return ids.map((id) => map.get(id) ?? null);
    })
  };
}

const Ticket = {
  device: (ticket, _args, context) => {
    if (!ticket.deviceId) return null;
    return context.loaders.deviceById.load(ticket.deviceId);
  }
};

这样 100 个设备查询就可能变成:

text
1 次查工单
+ 1 次批量查设备
= 2 次访问后端

总台比喻很好理解:

  • 错误做法:每位接待员都单独给设备中心打电话
  • 正确做法:总台把一批设备编号攒起来,一次发给设备中心

GraphQL 本身不会自动消灭 N+1。
它只是让你更容易暴露出这个问题。
真正解决它,靠的是:

  • 批量加载
  • 请求级缓存
  • 合理的数据访问层
  • 必要时把 selection set 翻译成更高效的底层查询