percent_rank()计算单行在排序后的位置比例,公式为(rank−1)/(total_rows−1),结果为0.0–1.0小数,首行恒0.0、末行(total_rows>1时)恒1.0,不反映分布密度或频次统计。

PERCENT_RANK() 不计算“百分比分布”,它只算单行在排序后的位置比例,结果是 0.0 到 1.0 的小数,不是直方图、密度或频次统计。
PERCENT_RANK() 的计算逻辑就是 (rank - 1) / (total_rows - 1)
它不统计有多少行落在某个区间,而是给每一行打一个“相对坐标”:
-
rank是该行按ORDER BY得到的名次(等同于RANK()值,相同值并列且跳号) -
total_rows是当前窗口内总行数(由PARTITION BY决定,没写就是全表) - 首行永远是
0.0(因为rank = 1→(1-1)/(n-1) = 0) - 末行在
total_rows > 1时永远是1.0(因为rank = n→(n-1)/(n-1) = 1) - 单行窗口返回
0.0(MySQL 标准定义,不是错误)
例如 5 行数据按分数升序排:[70, 70, 80, 90, 100],两个 70 并列 rank=1,它们的 PERCENT_RANK() 都是 (1-1)/(5-1) = 0.0;100 是 rank=5,结果是 (5-1)/(5-1) = 1.0。
为什么不能用 PERCENT_RANK() 做分桶或分布统计?
它不具备聚合能力,也不产生分组频次。常见误用场景:
- 想看“分数在 80–90 的用户占多少%” → 错,该用
COUNT(CASE WHEN score BETWEEN 80 AND 90 THEN 1 END) / COUNT(*) - 想画箱线图或分位直方图 → 错,该用
NTILE(4)或HISTOGRAM(MySQL 8.0+ 不原生支持,需客户端聚合) - 以为
PERCENT_RANK() = 0.5就是中位数 → 错,它只是标记最靠近中间位置的行,不保证值等于中位数 - 拿它和
CUME_DIST()混用判断覆盖比例 → 错,二者语义不同:PERCENT_RANK()是“比它小的占比”,CUME_DIST()是“≤它的占比”
真实分布分析必须配合 GROUP BY + 聚合函数,或先用 NTILE() 分桶再计数。
实际能用 PERCENT_RANK() 干什么?
它适合轻量级相对定位,前提是接受其线性映射特性:
- 筛选“前 5% 高分用户”:
PERCENT_RANK() OVER (ORDER BY score DESC) >= 0.95 - 标记每个用户在其部门内的绩效相对位置,用于可视化色阶(如红→黄→绿)
- 和
RANK()对比看数据是否集中:如果大量用户PERCENT_RANK()接近 0.0,说明头部效应强 - 做跨时间周期的可比排名:比如把本月和上月数据 UNION 后统一排序算
PERCENT_RANK(),避免因基数变化导致排名失真
注意:只要 ORDER BY 列有 NULL,默认排最前(ASC)或最后(DESC),会拉偏分母,务必提前 WHERE score IS NOT NULL。
容易被忽略的 MySQL 8.0 兼容细节
不是所有“支持窗口函数”的 MySQL 8.0 实例都开全了功能:
- 确认版本:
SELECT VERSION();必须 ≥8.0.2(早期 8.0.0/8.0.1 有 bug,PERCENT_RANK()在空窗口可能返回 NULL 而非 0) - 不能出现在
WHERE、GROUP BY、HAVING中,只能在SELECT或外层ORDER BY里用 - 若用
PARTITION BY dept ORDER BY salary DESC,而dept字段本身含 NULL,则该 NULL 会被单独分到一个分区,且该分区只有一行 → 全是0.0,容易误判为“高薪” - 大数据量下无
PARTITION BY会触发全表排序,执行计划里看到Sort节点就要警惕 IO 和内存开销
真正需要分布形态时,别硬套 PERCENT_RANK() —— 它只负责标位置,不负责数密度。











