PERCENT_RANK 返回0到1之间的小数,因其定义为“比当前行小的行数占比”,计算公式为(rank-1)/(total_rows-1),首行rank=1得0,末行rank=total_rows得1,单行时分母为0故定义返回0。

PERCENT_RANK 为什么返回 0 和 1 之间的小数?
PERCENT_RANK 不是「第几名」,而是「比你小的人占多少比例」。它的计算逻辑是:(rank - 1) / (total_rows - 1),其中 rank 是当前行在 ORDER BY 中的顺序位置(从 1 开始),total_rows 是窗口内总行数。所以最小值永远是 0(首行,rank=1),最大值永远是 1(末行,且 total_rows > 1)。如果只有一行,PERCENT_RANK 直接返回 0 —— 这不是 bug,是定义使然。
常见误用:拿它当「前 10% 用户」直接写 PERCENT_RANK() 。这其实漏掉了并列情况 —— 多个用户共享同一 <code>rank 时,PERCENT_RANK 值相同,但实际覆盖人数可能远超 10%。
按消费金额分组计算全平台用户百分比排名
典型场景:想看每个用户的累计消费在全体用户中的相对位置,且需排除测试账号、无效 ID。
- 必须用
OVER子句定义窗口,例如OVER (ORDER BY total_spent DESC)—— 注意排序方向:高消费排前面,其PERCENT_RANK才更小(因为「比他消费少的人占比低」) - 过滤要在窗口函数外做,否则
COUNT(*)基数不准。正确写法是先WHERE user_type != 'test',再套窗口 - MySQL 8.0+、PostgreSQL、SQL Server 都支持;SQLite 不支持,别踩坑
SELECT user_id, total_spent, PERCENT_RANK() OVER (ORDER BY total_spent DESC) AS pct_rank FROM users WHERE status = 'active' AND user_id NOT LIKE 'tmp_%';
和 RANK()、CUME_DIST() 的关键区别在哪?
三者都依赖排序,但语义完全不同:
-
RANK()返回跳跃式整数排名(如 1,1,3,4),适合「并列第 1 名」这类表述 -
CUME_DIST()计算「小于等于当前值的行数占比」,公式是number_of_rows_preceding_or_peer / total_rows,结果范围也是 [0,1],但末尾一定是 1,且对重复值更平滑 -
PERCENT_RANK()对重复值一视同仁 —— 所有相同total_spent的用户得到完全相同的百分比值,哪怕他们横跨不同业务线
举个例子:5 行数据,值为 [100,100,90,80,70],两个 100 并列 rank=1,则它们的 PERCENT_RANK 都是 (1-1)/(5-1)=0.0;而 CUME_DIST 对这两个 100 都返回 2/5=0.4。
实际业务中容易被忽略的边界问题
真实数据常有空值、异常长尾、多租户混排,不处理会扭曲百分比含义:
-
NULL默认排在最前(ORDER BY ... DESC时),导致大量用户被错误归为「0%」—— 显式加WHERE total_spent IS NOT NULL - 全平台数据量极大时,
PERCENT_RANK仍基于窗口内总行数计算,但如果你只取 TOP 1000 查看,total_rows就变成 1000,结果毫无参考价值 —— 必须确保窗口覆盖完整目标集 - 跨区域或子品牌分析时,别直接套用全平台
PERCENT_RANK。比如海外用户消费中位数远高于国内,混在一起算出的「前 5%」可能全是海外账号 —— 按PARTITION BY region分开算更合理
真正难的不是写对函数,而是确认「这个百分比到底在回答哪个业务问题」:是找高价值用户?还是监控分布偏移?前者需要稳定分位点(考虑用 PERCENTILE_CONT),后者才适合 PERCENT_RANK 的相对位置视角。

















