累计百分比的本质是“当前累计和 ÷ 总和”,需用sum() over(order by ...)计算分子,分母必须为静态总和(如sum() over()或子查询),严禁分子分母同构导致结果恒为1,须防null、除零及整数截断。

累计百分比的本质是“当前累计和 ÷ 总和”
窗口函数本身不直接提供 PERCENT_RANK() 或 CUME_DIST() 以外的“累计百分比”函数,但你可以用 SUM() OVER() 配合总和计算手动构造。关键不是套公式,而是理解分母该用哪个聚合结果:必须是整个分区的固定总和,不能是动态窗口的累计和。
常见错误是写成 SUM(col) OVER (ORDER BY x) / SUM(col) OVER (ORDER BY x ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) —— 这样分母和分子一样,结果永远是 1。
- 分母必须用
SUM(col) OVER ()(无PARTITION BY且无ORDER BY),确保它是全量静态和 - 分子用
SUM(col) OVER (ORDER BY key ASC ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) - 如果数据按时间排序,
key通常是时间字段;若按金额排序,则用金额字段 - 注意 NULL 值:
SUM()自动忽略 NULL,但如果你的业务要求把 NULL 当 0 处理,得先COALESCE(col, 0)
PostgreSQL 和 MySQL 8.0+ 的写法一致,但 SQLite 不支持
只要数据库支持标准窗口函数(PostgreSQL、SQL Server、Oracle、MySQL 8.0+、Trino、BigQuery),语法完全通用。SQLite 直到 3.25 才开始支持窗口函数,但累计百分比这种带 ROWS 框架的写法在旧版 SQLite 中会报错:near "ROWS": syntax error。
- 确认版本:MySQL 用
SELECT VERSION();,PostgreSQL 用SELECT version(); - MySQL 5.7 及更早版本不支持任何窗口函数,强行使用会报错
ERROR 1064 - 如果必须兼容老版本 MySQL,只能用自连接或变量模拟,性能差且不可靠
遇到 ORDER BY 字段重复时,累计百分比可能跳变
当 ORDER BY 字段存在重复值(比如多个订单同一天),SUM() OVER (ORDER BY date) 默认采用“群组式累积”:同一日期的所有行共享相同的累计值,下一日才更新。这符合多数业务逻辑(如“截至当日的累计销售额”),但如果你需要严格逐行递增(比如按插入顺序),就得加唯一排序键。
- 安全做法:在
ORDER BY中追加主键或时间戳,例如ORDER BY date, id - 否则,同一
date下三行数据,SUM() OVER会把三行的amount全加到第一行的累计值里,后两行显示相同结果 - 想验证是否跳变?查
ROW_NUMBER() OVER (ORDER BY date)和ROW_NUMBER() OVER (ORDER BY date, id)是否一致
别漏掉除零保护和小数精度控制
如果总和为 0(比如全负数再加个 0 值,或空表),直接除会得到 NULL 或报错(取决于数据库)。另外,默认浮点精度可能显示一长串小数,而业务通常要两位小数的百分比。
- 用
NULLIF(SUM(col) OVER (), 0)替代分母,避免除零;再配合COALESCE(..., 0)返回 0% - 转百分比:乘 100 后用
ROUND(..., 2),不要用CAST(... AS DECIMAL(5,2))—— 后者在某些引擎里会截断而非四舍五入 - 示例片段:
ROUND(100.0 * SUM(amount) OVER (ORDER BY dt ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) / NULLIF(SUM(amount) OVER (), 0), 2)
实际跑起来之前,先 SELECT SUM(col) OVER () 看一眼分母是不是你预期的值——这个动作花不了两秒,但能避开一大半线上计算错误。











