用 ROW_NUMBER() 配合 PARTITION BY 可精准实现组内排序取 Top 10%,需先计算每组总行数,再与唯一序号比较;RANK() 和 DENSE_RANK() 因并列问题不适用。

用 PARTITION BY + ROW_NUMBER() 实现组内排序取 Top 10%
直接用 ROW_NUMBER() 配合 PARTITION BY 是最可控的方式,尤其当“前 10%”需要严格按行数比例截断时。注意:必须先算出每组总行数,再和序号比较。
常见错误是误用 RANK() 或 DENSE_RANK() —— 它们无法处理重复值导致的“并列挤占名额”问题;而 ROW_NUMBER() 保证每行唯一序号,便于做整数比例截断。
- 先用子查询或 CTE 算出每组的
COUNT(*) - 在外层用
ROW_NUMBER() OVER (PARTITION BY group_col ORDER BY sort_col DESC)编号 - 用
rn 过滤(<code>CEIL防止整除丢精度,如 15 行应取 2 行而非 1 行)
WITH grouped AS (
SELECT *,
COUNT(*) OVER (PARTITION BY dept) AS total_cnt,
ROW_NUMBER() OVER (PARTITION BY dept ORDER BY salary DESC) AS rn
FROM employees
)
SELECT dept, name, salary
FROM grouped
WHERE rn <= CEIL(0.1 * total_cnt);
PERCENT_RANK() 更适合“相对位置”语义的 Top 10%
如果业务上“前 10%”指的是“排名在 90 分位数以上的记录”,而不是“取整数行”,PERCENT_RANK() 更贴切——它返回的是当前行在组内的归一化排名(范围 [0, 1)),0 表示组内第一,接近 1 表示末尾。
注意:PERCENT_RANK() 对重复值会给出相同结果,且首尾固定为 0 和接近 1 的值;它不依赖总行数,但对极小分组(如只有 2 行)可能失效(此时最大 PERCENT_RANK 是 1,不是 0.9)。
- 值为 0 的行一定是组内最高(无论是否重复)
- 过滤条件写成
PERCENT_RANK() < 0.1才是“前 10%”,不是<= 0.1 - MySQL 8.0+、PostgreSQL、SQL Server 支持;SQLite 不支持
SELECT dept, name, salary
FROM (
SELECT *,
PERCENT_RANK() OVER (PARTITION BY dept ORDER BY salary DESC) AS pr
FROM employees
) t
WHERE pr < 0.1;聚合后取 Top 10% 要先 GROUP BY 再排序,不能颠倒顺序
如果目标是“每个部门中,工资总和最高的前 10% 的部门”,这是跨组聚合再截断,不是组内排序。容易错写成在明细层排序后聚合,导致逻辑错误。
关键点:聚合必须在排序之前完成;否则你截的是明细行,不是聚合结果。
- 第一步
GROUP BY dept计算SUM(salary) - 第二步用窗口函数对聚合结果排序编号:
ROW_NUMBER() OVER (ORDER BY SUM(salary) DESC) - 第三步关联总组数(用
COUNT(*) OVER())算阈值
WITH dept_sum AS (
SELECT dept, SUM(salary) AS total_salary,
COUNT(*) OVER() AS total_depts
FROM employees
GROUP BY dept
),
ranked AS (
SELECT *,
ROW_NUMBER() OVER (ORDER BY total_salary DESC) AS rn
FROM dept_sum
)
SELECT dept, total_salary
FROM ranked
WHERE rn <= CEIL(0.1 * total_depts);不同数据库对 CEIL 和窗口函数的支持差异要手动验证
PostgreSQL 和 SQL Server 默认支持 CEIL;MySQL 用 CEILING();SQLite 没有 CEIL,得用 CAST((0.1 * total_cnt) AS INTEGER) + (CASE WHEN 0.1 * total_cnt > CAST(0.1 * total_cnt AS INTEGER) THEN 1 ELSE 0 END) 模拟。
更隐蔽的问题是:某些旧版数据库(如 MySQL 5.7)根本不支持窗口函数,强行写 ROW_NUMBER() 会报错 FUNCTION xxx.ROW_NUMBER does not exist。这时只能用变量模拟,但并发下不可靠。
- 执行前先查版本:
SELECT VERSION();(MySQL)、SELECT version();(PostgreSQL) - 确认
SELECT ROW_NUMBER() OVER() FROM (SELECT 1) t;是否能跑通 - 生产环境若无法升级,建议改用应用层分页+内存排序,避免 SQL 层硬扛
实际中最容易被忽略的是“10% 的向下取整陷阱”:比如某组 9 行,10% 是 0.9,直接 FLOOR(0.1 * 9) 得 0,结果一条不取。必须用 CEIL 或等价逻辑,哪怕只有一行也要至少取一行。

















