GraphQL 的字段式执行很优雅,但也容易产生著名的 N+1 问题。
看一个查询:
query {
tickets(status: OPEN) {
id
title
device {
id
name
}
}
}如果有 100 张工单,最天真的 resolver 写法可能会这样干:
- 先查一次工单列表
- 然后每张工单各自再查一次设备
也就是:
1 次查工单
+ 100 次查设备
= 101 次访问后端这就是 N+1。
天真写法:
const Ticket = {
device: async (ticket, _args, context) => {
return context.db.getDeviceById(ticket.deviceId);
}
};更合理的做法,是批量调度。常见方案就是 DataLoader 模式:
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 个设备查询就可能变成:
1 次查工单
+ 1 次批量查设备
= 2 次访问后端总台比喻很好理解:
- 错误做法:每位接待员都单独给设备中心打电话
- 正确做法:总台把一批设备编号攒起来,一次发给设备中心
GraphQL 本身不会自动消灭 N+1。
它只是让你更容易暴露出这个问题。
真正解决它,靠的是:
- 批量加载
- 请求级缓存
- 合理的数据访问层
- 必要时把 selection set 翻译成更高效的底层查询