查询 10 个商品只需 120ms,查询 50 个商品却超过 3 秒。数据库监控显示同一条 SQL 换着 product_id 执行几十次。这就是 GraphQL 常见的 N+1:列表本身查一次,每个 item 的自定义字段又各查一次。
先证明慢的是哪个字段
从最小 query 开始,只保留 items 的 id;然后每次加回一个字段,记录响应时间、SQL 数量和 resolver 耗时。不要一上来分析完整 PWA 请求,因为多个字段、fragment 与内联查询会把问题混在一起。
在预发布启用数据库 profiler 或 APM,把 SQL 指纹聚合。若商品数从 10 增到 50,某条 SQL 次数也从 10 墠到 50,基本可以锁定对应 resolver。生产环境采样时要限制日志,不记录客户 token 和完整变量。
普通 Resolver 为什么容易制造 N+1
字段 resolver 会针对父节点逐个调用。如果每次都用 Repository 加载实体,除了 SQL,还会触发插件、扩展属性和事件。Repository 适合清晰的单实体业务边界,不代表适合循环中批量读取。
先收集当前批次所有父实体 ID,再一次查询所需数据,以 ID 建 map 返回。Magento GraphQL 提供 BatchResolver 和 BatchServiceContractResolver 机制,用于把同一字段的多个请求合并。关键不是接口名字,而是保证批量服务只查一次,并能保持输入输出顺序与缺失值语义。
批量之后还要处理重复字段与缓存
同一个请求中不同 fragment 可能访问同一业务数据,可在请求生命周期内做小范围 memoization,但不要用跨客户的静态缓存保存个性化结果。可公开缓存的 query 需要正确 cache identity;含客户、购物车或权限数据的字段不能误放进公共缓存。
防止查询本身无限膨胀
即使没有 N+1,客户端一次请求数百 items、深层嵌套关联,也会消耗大量资源。为分页设置合理上限,评估查询复杂度和超时,并让前端只取当前页面真正使用的字段。返回巨大 payload 后再由浏览器丢掉大半,是另一种浪费。
改造后用 10、50、100 个 item 做曲线:SQL 数量应接近固定或按少量批次增长,响应时间不再线性爆炸,结果与旧 resolver 一致,缓存失效也正确。性能优化不能以漏字段或串客户数据为代价。

