用 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 <h3>
<code>PERCENT_RANK()</code> 更适合“相对位置”语义的 Top 10%</h3><p>如果业务上“前 10%”指的是“排名在 90 分位数以上的记录”,而不是“取整数行”,<code>PERCENT_RANK()</code> 更贴切——它返回的是当前行在组内的归一化排名(范围 [0, 1)),0 表示组内第一,接近 1 表示末尾。</p><p>注意:<code>PERCENT_RANK()</code> 对重复值会给出相同结果,且首尾固定为 0 和接近 1 的值;它不依赖总行数,但对极小分组(如只有 2 行)可能失效(此时最大 <code>PERCENT_RANK</code> 是 1,不是 0.9)。</p>
- 值为 0 的行一定是组内最高(无论是否重复)
- 过滤条件写成
PERCENT_RANK() 才是“前 10%”,不是 <code> - 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 <h3>聚合后取 Top 10% 要先 <code>GROUP BY</code> 再排序,不能颠倒顺序</h3><p>如果目标是“每个部门中,工资总和最高的前 10% 的部门”,这是跨组聚合再截断,不是组内排序。容易错写成在明细层排序后聚合,导致逻辑错误。</p><p>关键点:聚合必须在排序之前完成;否则你截的是明细行,不是聚合结果。</p>
- 第一步
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 <h3>不同数据库对 <code>CEIL</code> 和窗口函数的支持差异要手动验证</h3><p>PostgreSQL 和 SQL Server 默认支持 <code>CEIL</code>;MySQL 用 <code>CEILING()</code>;SQLite 没有 <code>CEIL</code>,得用 <code>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)</code> 模拟。</p><p>更隐蔽的问题是:某些旧版数据库(如 MySQL 5.7)根本不支持窗口函数,强行写 <code>ROW_NUMBER()</code> 会报错 <code>FUNCTION xxx.ROW_NUMBER does not exist</code>。这时只能用变量模拟,但并发下不可靠。</p>
- 执行前先查版本:
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 或等价逻辑,哪怕只有一行也要至少取一行。










