多级分组排名应选rank()或dense_rank()而非row_number():rank()跳过重复名次,dense_rank()连续编号;必须配合PARTITION BY和ORDER BY,且WHERE筛选需用子查询避免破坏分组。

rank() 和 dense_rank() 在多级分组中行为差异明显
说到多级分组排名,真正的难点往往不在于“怎么写”,而在于“选哪个函数”。rank() 和 dense_rank() 虽然都是排名,但处理并列时的逻辑截然不同:rank() 会跳过重复名次后的编号,而 dense_rank() 则坚持连续编号。举个例子,如果同一组内有三个并列第一,那么 rank() 给出的下一个名次就是第四,而 dense_rank() 给出的则是第二。
这可不是简单的语法差异,它直接关系到业务口径。在实际工作中,当业务方提出“并列之后下一个名次怎么算”时,必须先搞清楚他们到底要的是“跳号”还是“顺位”。前者常见于体育比赛的奖牌榜,后者则多用于考试成绩单这类场景。千万别图省事默认使用 row_number(),这个函数根本不处理并列,只是机械地按顺序编号,最终结果很容易引发数据质疑。
rank():更适合强调“名次层级”的场景。比如销售榜单,并列冠军之后,下一个直接就是第四名,这能清晰地拉开差距。dense_rank():更适合强调“位置序号”的场景。比如按“城市+月份”统计销量TOP3,必须确保每组最多只取三行,连续编号才能保证逻辑正确。- 无论选择哪个函数,都必须配合
PARTITION BY来指定多级分组字段,例如PARTITION BY region, product_category,这是实现分组排名的基石。
PARTITION BY 多字段顺序影响结果可读性
别以为 PARTITION BY 后面字段的顺序无关紧要。从数据库计算的角度看,PARTITION BY dept, team, year 和 PARTITION BY year, dept, team 在逻辑上是等价的,都不会报错。但问题在于,它们输出的结果集在排序和可读性上可能天差地别。
如果结果集没有显式地使用 ORDER BY 进行全局排序,那么分组内的行序是不确定的。这时,一个糟糕的字段顺序,可能会让你看到“2023年A组第1名”紧挨着“2022年A组第1名”,但中间却夹杂着大量2023年B组的数据,排查起来简直是一场噩梦。
一个实用的建议是:把基数高、变动频率低的维度字段放在前面。通常来说,时间维度(如 year、quarter)比组织维度(如 team)更稳定,也更符合大多数人的分析阅读习惯。
- 分组字段的顺序不影响计算的正确性,但深刻影响结果集的天然聚类程度。
- 务必在窗口函数内部使用
ORDER BY子句明确排序依据,例如ORDER BY revenue DESC, employee_id。 - 需要警惕的是,如果分组字段包含 NULL 值,不同数据库的处理方式不同:
PostgreSQL默认将 NULL 排在最前,MySQL 8.0+可以使用NULLS LAST语法控制,而SQL Server则不支持该语法。
WHERE 和窗口函数不能直接互换位置
这是一个非常典型的陷阱:想实现“先筛选再排名”,却错误地把筛选条件放在了 WHERE 子句中。比如,只想统计“销售额大于10000”的员工的排名。如果直接写 WHERE sales > 10000,数据库会先过滤掉所有不达标的员工,然后再对剩下的“幸存者”进行分组排名。这样一来,你得到的“第1名”,只是该组达标者中的第一,而非全组真正的第一。
其实,大部分业务的真实需求是:“让所有人参与排名计算,但最终只展示达标者的排名结果。” 要实现这个逻辑,就必须借助子查询或公共表表达式(CTE):在内层完成全量排名计算,在外层进行结果筛选。
SELECT dept, name, sales, rk
FROM (
SELECT dept, name, sales,
dense_rank() OVER (PARTITION BY dept ORDER BY sales DESC) AS rk
FROM employees
) t
WHERE sales > 10000;
- 从逻辑执行顺序来看,窗口函数的计算发生在
FROM和WHERE之后,但在最终的ORDER BY和LIMIT之前。 - 不能在
GROUP BY聚合之后直接使用窗口函数,除非再嵌套一层查询(因为聚合已经改变了行数)。 - 在某些旧版本的 MySQL 中,
ORDER BY子句的稳定性无法保证,存在潜在风险。
性能敏感点:ORDER BY 字段未建索引时开销陡增
当“多级分组”、“排名计算”和“大表”这三个要素凑在一起时,性能瓶颈往往就出现在 ORDER BY 上。窗口函数内部需要为每一个分组进行局部排序,如果 PARTITION BY a, b ORDER BY c 中的排序字段 c 没有合适的索引,数据库极有可能被迫进行磁盘排序。对于百万级别的数据表,查询响应时间可能从毫秒级陡增至秒级。
当然,并非所有组合都需要建立索引。策略是优先为高频使用的“分组+排序”组合创建覆盖索引。例如,经常按 (region, year) 分组并按 revenue DESC 排名,那么建立联合索引 INDEX(region, year, revenue) 会非常有效。这里有个关键细节:索引的前导字段必须与 PARTITION BY 中的字段前缀相匹配。
- 单列索引对多级分组排名的优化效果有限,联合索引才是正解。
- 在 PostgreSQL 中,如果窗口函数中写了
ORDER BY revenue DESC NULLS LAST,那么对应的索引也必须声明为DESC NULLS LAST才能被完美利用。 - 如果业务需求仅仅是取出每组的 TOP N 行,有时可以考虑使用
LATERAL JOIN或数据库特有的FETCH FIRST N ROWS ONLY语法,配合索引,其性能可能优于计算全量排名。
话说回来,在实际编写多级排名查询时,最容易忽略的一个细节是:你选定的分组字段,是否真的能唯一、准确地标识出业务意义上的独立单元?举个例子,用 user_id 和截断后的 date(如“2023-10-01”)分组,但如果源数据的时间戳是精确到毫秒的,截断操作就可能导致本应属于不同时间片的数据被错误地合并到同一组。这种错误不会引发任何报错,但统计口径已经悄悄发生了偏移,这才是最需要警惕的地方。
