一对多连接的行数放大
连接「多」侧之后结果会按多行展开:订单表连接明细表后,一个订单会重复出现多次,每个明细一行。这个特性本身是正常的,问题在于统计时容易重复计数——订单数被算成明细行数,订单金额被重复累加。处理办法是先把多侧聚合到需要连接的粒度,再连接主表。判断当前连接是否放大,可以分别数左表行数与连接后行数,两者不等就说明发生了放大。
基本信息
| 现象 | 一个主表行匹配多个明细行,结果按多行展开 |
|---|---|
| 常见后果 | COUNT 偏大、SUM 被重复累加 |
| 判断方法 | 对比左表行数与连接后行数 |
| 缓解办法 | 先把多侧聚合到主表粒度,再连接 |
| 替代方案 | 用标量子查询或窗口函数计算组内指标 |
| 适用平台 | 所有关系型数据库的连接语义一致 |
典型使用场景
- 订单与订单明细:取明细时必然放大行数
- 用户与登录日志:一个用户有多条日志
- 商品与库存批次:同一商品多批次
- 报表口径核对:解释为什么总数比预期大
- 漏斗分析:多步骤事件表关联后行数变化
- 排查重复数据:连接放大常被误判成数据重复
示例
示例
-- 错误:订单金额被明细行数放大
SELECT COUNT(*) AS rows_cnt, # 是明细行数,不是订单数
SUM(o.amount) AS amount_sum # 订单金额被重复累加
FROM orders o
JOIN order_items i ON i.order_id = o.id;
-- 正确一:先聚合明细到订单粒度,再连接
SELECT COUNT(*) AS order_cnt, SUM(o.amount) AS amount_sum
FROM orders o
LEFT JOIN (SELECT order_id, SUM(qty) AS qty FROM order_items GROUP BY order_id) t
ON t.order_id = o.id;
-- 正确二:用 EXISTS 判断是否存在明细,不放大行数
SELECT COUNT(*) FROM orders o
WHERE EXISTS (SELECT 1 FROM order_items i WHERE i.order_id = o.id);
-- 正确三:需要去重计数时用 COUNT(DISTINCT o.id)
SELECT COUNT(DISTINCT o.id) FROM orders o JOIN order_items i ON i.order_id = o.id;放大后的影响
| 统计写法 | 结果 | 建议 |
|---|---|---|
| COUNT(*) | 明细行数 | 改用 COUNT(DISTINCT 主键) |
| SUM(主表金额) | 重复累加 | 先去重或先聚合 |
| SUM(明细数量) | 正确 | 本来就是明细口径 |
| AVG(主表值) | 被明细数量加权 | 注意权重是否符合口径 |
| MAX / MIN | 不受影响 | 可直接用 |
最直观的排查手段就是数行数:SELECT COUNT(*) FROM 左表 与连接后的行数对比,不一致即说明发生了放大。
COUNT(DISTINCT 主键) 能解决计数口径问题,但 SUM 无法用 DISTINCT 简单修好,正确做法仍然是先聚合到主表粒度再连接。
连接放大后的结果被下游报表反复引用,会形成难以追溯的口径错误;报表 SQL 上线前应先验证行数与预期一致。
使用要点
- 连接前先想清楚结果粒度:一行代表主表记录还是明细
- 涉及金额与计数时先聚合再连接
- 把聚合放进子查询或 CTE,主查询只做关联与过滤
- 报表 SQL 上线前对比行数与主表行数是否一致
- 口径写进注释,说明每个指标对应哪一层的粒度
- 用 COUNT(DISTINCT) 只是补丁,根本上要让连接粒度与统计粒度一致