返回博客

后端 SQL 已聚合正确,行被炸开的元凶却在前端 flatMap

需求排期压着,后台报表突然出 bug:同一个实体(门店、订单、客户……)在表格里分裂成了两行甚至更多行。 第一反应——"SQL 没聚合好,加个 GROUP BY 或补个 JOIN"。写完 diff,打开页面,行还在,甚至更多了。

这是一次让人绕了一大圈的反直觉踩坑。

现象

后台报表面板,同一个实体 ID(如某门店编号)出现在两行,分属不同的分组标签。数据来自同一个后端接口,后端查询逻辑最近没动过,但前端在一个月前做过「双视角切换」需求(可以在"按门店维度"和"按跟进人维度"之间 radio-group 切换)。

根因

后端 SQL 其实早就正确。主查询早已做了 JOIN 绑定表、GROUP BY 实体 ID,返回的每个实体只有 1 行。

问题出在"双视角"功能加入时,后端额外跑了 2-3 个 by_dimension breakdown query(按部门、按跟进人分切片),前端用 flatMap 把主行和切片叉乘:

// 炸开点:1 实体 × N 部门切片 = N 行
const rows = mainRows.flatMap(r =>
  r.by_dept.map(d => ({ ...r, deptName: d.name, deptCount: d.count }))
);

再配上 :span-method 做视觉合并,表象上看像是"合并单元格",本质上数据已经是 N 行了。

坑:改后端聚合治不了前端 flatMap 炸行

「UI 有多行」不等于「后端返回多行」。加 GROUP BY、加 JOIN、加 GROUP_CONCAT 改完后症状依然存在——因为后端一直只返回 1 行,是前端 flatMap 把它展开成了 N 行。改错方向不仅无效,还可能破坏已有正确的聚合逻辑。

排查口诀(30 分钟定根因)

Step 1 — 绕开前端直接跑后端 SQL

把接口对应的 controller action 里的 SQL 单独拿出来,在数据库控制台执行,看原始返回行数。

  • 每个实体只有 1 行 → 后端正确,前端在炸 → 跳到 Step 3
  • 每个实体有 N 行 → 后端真的有问题(罕见)→ Step 2

Step 2 — 检查是否有额外的 breakdown query

在 action 函数体内搜多个独立 ->query() / ->select() 调用,如果发现 by_dept / by_user / by_channel 之类的切片查询,那些就是前端炸开的数据源。

Step 3 — 前端 grep 炸开点

grep -rn "flatMap\|reduce.*push\|span-method" src/views/<page>/

三大典型炸开模式:

  1. flatMap 行倍化:主行 × 切片行 = N 行
  2. computed transform 在数据层把 1 行扩展成 N 行
  3. :span-method 视觉合并——表象是合并,数据已经是 N 行

Step 4 — 看 git log / ISSUE 历史

git log --all --oneline -- <page-file> | head -20

如果发现近期有「加 X 视角」「双维度切换」「按 Y 拆分」主题的提交,那一轮加的代码就是现在的元凶。

正解

正解:删前端炸开逻辑,而不是改后端聚合

优先级从高到低:

  1. 前端 flatMap + span-method + radio-group + by_dimension fetcher(数据源已对,把展示层还原即可)
  2. 后端 by_dimension breakdown query(它们只给前端炸开消费,前端不炸了它们就是死代码,顺带减少 1-3 个 SQL roundtrip,接口延迟明显下降)
  3. ~~加后端字段/JOIN~~ — 后端已正确,加只引入冗余
  4. ~~改后端 GROUP BY~~ — 已聚合到 1 行,改了反而破坏

如果用户要求「保留原有视角,新增一个不炸行的面板」,正确姿势是:后端新加独立 action(复用现有主 SQL,不复用 breakdown query)+ 前端新加独立组件(无 flatMap,无 span-method),两套代码完全隔离,不交叉。

验证

修复后确认:

  • 前端:目标 .vue/.tsx 文件内 flatMapspan-method 命中 0 次
  • 后端:action 内 breakdown query 已删除或仅在保留视角函数内
  • 数据:打开页面,1 个实体只出现 1 行
  • 性能:接口响应时间下降(少了 2-3 个 SQL roundtrip)

一句话外卖

看到 UI 行炸开,先跑裸 SQL 验后端是否真的有问题;后端 1 行 / 实体,则 100% 是前端 flatMap 在炸——不要往后端加聚合,要往前端删展开。