dense_rank() 实现“并列不跳号”排名,与row_number()(强制唯一)和rank()(并列跳号)本质不同;必须配合over(order by...)使用,不可与group by混用,需通过cte或子查询先聚合再排名。

为什么 DENSE_RANK() 不是 ORDER BY + ROW_NUMBER()?
因为 DENSE_RANK() 的核心是「并列不跳号」,而 ROW_NUMBER() 强制唯一编号,RANK() 并列但会跳号。比如分数 [100, 95, 95, 90],三者结果分别是:[1,2,2,3]、[1,2,3,4]、[1,2,2,4]。实际做销售榜、年级排名时,跳号(RANK)会让第 3 名看起来像“断层”,用户困惑;而 ROW_NUMBER 把并列两人硬拆成第 2 和第 3,违背业务语义。
实操建议:
-
DENSE_RANK()必须配合OVER (ORDER BY ...),不能单独用 - 排序字段允许多列,如
OVER (ORDER BY dept_id, salary DESC),先按部门再按薪资 - 如果想按组内排名,必须加
PARTITION BY,否则全表统排
GROUP BY 后还能用 DENSE_RANK() 吗?
不能直接用。一旦用了 GROUP BY,非聚合字段不可引用,而 DENSE_RANK() 是窗口函数,依赖原始行粒度。常见错误是写成:
SELECT dept_id, DENSE_RANK() OVER (ORDER BY AVG(salary)) FROM emp GROUP BY dept_id;
这会报错:column "salary" must appear in the GROUP BY clause or be used in an aggregate function。根本矛盾在于:窗口函数运行在「分组前」的行集上,聚合后只剩一行,窗口无意义。
正确做法分两步:
- 先用子查询或 CTE 算出每组聚合值(如
AVG(salary)),保留dept_id - 在外层对这个结果集用
DENSE_RANK() OVER (ORDER BY avg_salary)
示例:
WITH dept_avg AS (
SELECT dept_id, AVG(salary) AS avg_salary
FROM emp GROUP BY dept_id
)
SELECT dept_id, avg_salary,
DENSE_RANK() OVER (ORDER BY avg_salary DESC) AS rank_no
FROM dept_avg;
ORDER BY 中 NULL 值怎么影响 DENSE_RANK() 结果?
默认情况下,NULL 被视为最大值(在 DESC 排序中排最前,在 ASC 中排最后),这会导致排名意外偏移。比如统计用户最后登录时间,last_login_at IS NULL 的用户可能被排成第 1 名,明显不合理。
解决方法统一用 NULLS LAST 或 NULLS FIRST 显式控制:
-
ORDER BY last_login_at DESC NULLS LAST:把未登录用户排到底部 -
ORDER BY status NULLS FIRST:把状态为空的记录优先展示 - 注意:PostgreSQL / Oracle / SQL Server 2022+ 支持该语法;MySQL 8.0 不支持
NULLS FIRST/LAST,需用COALESCE(status, 'zzz')替代
性能差?可能是没建对索引
DENSE_RANK() 本身不慢,慢的是它依赖的排序过程。如果 OVER (ORDER BY x, y) 字段没有索引,数据库就得临时排序整张表,IO 和内存开销陡增,尤其数据量超百万时延迟明显。
优化要点:
- 为
ORDER BY中的字段建联合索引,顺序严格匹配,如INDEX (dept_id, salary DESC) - 若还带
PARTITION BY dept_id,索引应以分区字段开头,再跟排序字段,否则无法加速分区内排序 - 避免在
ORDER BY里用函数,如ORDER BY UPPER(name),索引失效
真正容易被忽略的是:窗口函数的执行时机在 WHERE 之后、GROUP BY 之前,所以先过滤再排名能显著减少计算量——别在全表上跑 DENSE_RANK(),记得加 WHERE status = 'active' 这类条件。










