MySQL 8.0+ 用 GROUP BY 配合 SUM() OVER() 计算分组占比,需避免嵌套聚合错误;累计百分比必须显式 ORDER BY 和 ROWS UNBOUNDED PRECEDING;低版本需自连接模拟,NULL 分组需单独处理。

用 GROUP BY + 窗口函数算分组占比
MySQL 8.0+ 支持窗口函数,SUM() OVER() 是最直接的解法。先按分组聚合计数,再用窗口求总和,就能算出每个分组占总体的比例。
常见错误是直接在 GROUP BY 后用 SUM(count) —— 这会报错,因为 count 是聚合结果,不能嵌套聚合;必须用窗口函数跨行访问全局总和。
- 写法示例:
SELECT category, COUNT(*) AS cnt, COUNT(*) / SUM(COUNT(*)) OVER() AS ratio FROM orders GROUP BY category; -
SUM(COUNT(*)) OVER()中的双层COUNT(*)是合法的:内层按分组计数,外层窗口对所有分组结果求和 - 注意
ratio是小数,乘 100 并ROUND(..., 2)更适合展示百分比
累计百分比必须排序后用 ROWS UNBOUNDED PRECEDING
累计百分比不是简单累加 ratio,而是按某列顺序(比如分组名或数量)逐行叠加占比。不显式指定 ORDER BY 和窗口帧,SUM(ratio) OVER() 默认行为不可靠,可能返回乱序累计值。
面向 CRM 系统的 MySQL 8 数据库模式设计。当用户需要为客户关系管理(CRM)系统设计、评审、优化或生成数据库模式时,应使用此项技能。
- 正确写法需明确排序和帧范围:
SELECT category, cnt, ROUND(100 * ratio, 2) AS pct, ROUND(100 * SUM(ratio) OVER (ORDER BY cnt DESC ROWS UNBOUNDED PRECEDING), 2) AS cum_pct FROM ( SELECT category, COUNT(*) AS cnt, COUNT(*) / SUM(COUNT(*)) OVER() AS ratio FROM orders GROUP BY category ) t; -
ORDER BY cnt DESC决定累计顺序(比如按数量从高到低),漏掉这句会导致累计逻辑失效 -
ROWS UNBOUNDED PRECEDING表示“从第一行累加到当前行”,省略它可能只计算相邻几行
MySQL 5.7 或更低版本只能用自连接模拟窗口
没有窗口函数时,得靠自连接或变量实现累计。变量方案看似简洁,但 MySQL 5.7 中变量赋值顺序不保证,容易在并发或优化器重排后出错;自连接更稳定,只是性能差。
- 安全但稍慢的做法(自连接):
SELECT t1.category, t1.cnt, ROUND(100 * t1.cnt / t2.total, 2) AS pct, ROUND(100 * (SELECT SUM(t3.cnt) FROM (SELECT category, COUNT(*) AS cnt FROM orders GROUP BY category) t3 WHERE t3.cnt >= t1.cnt) / t2.total, 2) AS cum_pct FROM (SELECT category, COUNT(*) AS cnt FROM orders GROUP BY category) t1 CROSS JOIN (SELECT SUM(cnt) AS total FROM (SELECT COUNT(*) AS cnt FROM orders GROUP BY category) t) t2 ORDER BY t1.cnt DESC; - 如果硬要用变量,至少加
ORDER BY强制顺序:@cum := @cum + ratio前必须确保数据已排序,且初始化@cum := 0在同一查询中 - 这种写法在数据量大时明显变慢,因子查询反复执行
NULL 值和空分组会让占比计算失真
COUNT(*) 不统计 NULL,但若分组字段本身为 NULL,会被单独归为一组——这组会参与占比计算,但常被忽略,导致业务理解偏差。
- 检查是否有 NULL 分组:
SELECT category, COUNT(*) FROM orders GROUP BY category ORDER BY category IS NULL DESC; - 想排除 NULL 再算占比,得在子查询里加
WHERE category IS NOT NULL,否则 NULL 组的占比会挤占其他组数值 - 若用
COUNT(column)替代COUNT(*),会跳过该列 NULL 的行,但分组仍基于原字段,逻辑更难理清,不建议混用
实际跑起来,最容易出问题的是累计部分:没写 ORDER BY、用了变量却没控制执行顺序、或者把 SUM() OVER() 当普通聚合用。这些地方一错,百分比数字看着合理,累计线却乱跳。

















