percent_rank() 返回当前行在分组内的相对排名归一化值,范围是[0, 1),计算公式为(rank - 1) / (total_rows - 1),最小值为0,最大值趋近但不等于1。

PERCENT_RANK() 返回的是什么值
PERCENT_RANK() 不是直接返回“前5%”的布尔标记,而是对当前行在分组内的相对排名做归一化:最小值为 0,最大值趋近但不等于 1(因为定义是 (rank - 1) / (total_rows - 1))。所以“前5%”实际对应的是 PERCENT_RANK() ,不是 <code> —— 最后一名永远是 <code>0,第一名永远是 0,这点容易误判。
它必须配合 OVER 子句使用,且排序逻辑直接影响结果。没写 ORDER BY 就报错,写了但顺序和业务预期相反(比如按销量升序排却想取高销量 Top 5%),结果就全反了。
按类别分组计算前5%必须用 PARTITION BY
如果只写 PERCENT_RANK() OVER (ORDER BY score DESC),整个表被当做一个大组算排名,类别边界就没了。要按每个 category 独立计算,必须显式加上 PARTITION BY category:
SELECT id, category, score, PERCENT_RANK() OVER (PARTITION BY category ORDER BY score DESC) AS prk FROM products;
常见错误包括:
- 漏掉
PARTITION BY,导致跨类别污染排名 - 把
category放在GROUP BY里再套窗口函数——语法报错,窗口函数不能嵌套在聚合查询中 - 用
DISTINCT category先查出类别再循环执行——低效且难以维护,应一步到位
为什么 WHERE prk
当某类只有 1–19 行时,PERCENT_RANK() 的最小非零值可能远大于 0.05。例如只有 10 行,排名为 2 的行的 prk = (2-1)/(10-1) ≈ 0.111,此时 prk 永远不成立。
解决办法不是硬调阈值,而是换思路:用 ROW_NUMBER() 配合类别总行数算“硬性前5%”:
WITH ranked AS (
SELECT *,
ROW_NUMBER() OVER (PARTITION BY category ORDER BY score DESC) AS rn,
COUNT(*) OVER (PARTITION BY category) AS cnt
FROM products
)
SELECT * FROM ranked
WHERE rn <p>注意:<code>CEIL()</code> 确保至少取 1 条;<code>COUNT(*) OVER</code> 必须和 <code>PARTITION BY</code> 完全一致;<code>ROW_NUMBER()</code> 和 <code>PERCENT_RANK()</code> 在并列值处理上不同——前者强制唯一编号,后者给相同值相同排名,选哪个取决于业务是否允许并列进 Top 5%。</p><h3>性能与索引建议</h3><p>这类查询容易慢,尤其表大、类别多时。<code>PARTITION BY + ORDER BY</code> 本质要对每个类别内部排序,没索引就全表扫描+临时文件排序。</p><p>建复合索引能显著提速:</p>
-
INDEX (category, score)—— 覆盖最常用场景(倒序取 Top) - 如果常按
score ASC查 Bottom 5%,则建INDEX (category, score)依然有效(MySQL 8.0+ 支持降序索引,但普通升序索引也能用于ORDER BY ... DESC,只要字段顺序匹配) - 避免在
WHERE中对category用函数或隐式转换,否则索引失效
真正麻烦的是动态阈值(比如前端传 3% 或 10%)+ 多维度分组(category + region + year),这时候 PERCENT_RANK() 的语义清晰性反而成了负担,不如预计算排名快照表。











