连接基数与可信报表
可运行 SQL
sql
USE mysql_course;
SELECT
c.customer_id,
c.display_name,
COUNT(o.order_id) AS paid_order_count,
COALESCE(SUM(o.total_amount), 0) AS paid_total
FROM customer AS c
LEFT JOIN sales_order AS o
ON o.customer_id = c.customer_id
AND o.status = 'paid'
GROUP BY c.customer_id, c.display_name
ORDER BY c.customer_id;
WITH item_totals AS (
SELECT order_id, SUM(quantity * unit_price) AS item_total
FROM order_item
GROUP BY order_id
)
SELECT o.order_id, o.total_amount, i.item_total
FROM sales_order AS o
JOIN item_totals AS i ON i.order_id = o.order_id
WHERE o.total_amount <> i.item_total;机制解释
LEFT JOIN 先保留左侧客户。把右表状态过滤放在 ON 中,只限制匹配订单;放到 WHERE 会删除 NULL 扩展行,使结果表现得像内连接。
COUNT(column) 忽略 NULL,COUNT(*) 统计行。SUM 没有输入行时可能返回 NULL,因此展示层常用 COALESCE,但“无数据”是否等于零必须由业务定义。
ONLY_FULL_GROUP_BY 阻止查询选择无法由分组键唯一确定的列。它在保护结果语义,不应通过关闭 SQL mode 逃避建模问题。
真实边界
- 报表必须声明金额粒度、时间边界、时区和取消/退款规则。
- JOIN 正确性先于性能;索引只能加速错误答案。
- 浮点或跨币种金额不能直接求和;金额、币种和汇率时间点必须共同建模。
练习
- 写出保留零订单客户、仅统计 paid 订单的查询。
- 构造订单、订单项和支付的一对多乘积,解释重复金额来源。
- 找出订单头金额与订单项汇总不一致的数据。
验收清单
- 能明确说明每个结果行的业务粒度。
- 零订单客户不会被右表过滤意外删除。
- 多个一对多关系不会让金额重复累计。
深度加餐:报表错误通常是连接基数先错了
先算每个 JOIN 输入输出的行数,再写 SUM;否则金额会因为一对多连接被重复放大。
先做一个可观察实验
sql
WITH item_totals AS (
SELECT order_id, SUM(quantity * unit_price) AS items_total
FROM order_items
GROUP BY order_id
)
SELECT o.id, o.order_no, COALESCE(i.items_total, 0) AS items_total
FROM orders AS o
LEFT JOIN item_totals AS i ON i.order_id = o.id
ORDER BY o.id;预期:先按订单聚合明细,再连接订单主表;没有明细的订单仍显示 0,而不是被 INNER JOIN 吞掉。
原理下潜与工程边界
JOIN 只负责组合行,聚合负责定义粒度。函数依赖决定 GROUP BY 是否合理,ONLY_FULL_GROUP_BY 能帮助发现“选了未分组列”的歧义。
COUNT(*) 与 COUNT(column) 对 NULL 不同;多条一对多连接会形成笛卡尔放大;LEFT JOIN 后在 WHERE 过滤右表列会意外变成 INNER JOIN。
反例排错路线
逐步执行 CTE/子查询并统计行数;为每张表写主键唯一性检查;金额类报表补一条与业务账本对账的总和断言。
练习加码(仍不提供答案)
加码:写一份客户月度收入报表,既要包含零收入客户,又要避免订单与退款明细互相放大。
验收时请补充:一份可复现的环境/数据前提、正常结果、失败结果以及可回滚或清理动作。
继续查阅:MySQL 8.4 参考手册(外部资料已收录于证据账本) · InnoDB 文档(外部资料已收录于证据账本)