SQL 数据分析 · 指南 · 2026年10月30日 · 13 分钟阅读
ROW_NUMBER、RANK、DENSE_RANK 的区别:并列时怎么排名

ROW_NUMBER()、RANK() 和 DENSE_RANK() 都按照 OVER (ORDER BY ...) 里给出的顺序给行编号。在出现并列之前,三者的结果完全一样。 一旦并列,ROW_NUMBER 仍然给出唯一的编号(1、2、3、4),RANK 让并列的行同号然后跳号(1、2、2、4),DENSE_RANK 让并列的行同号但不跳号(1、2、2、3)。实际使用中:「恰好 N 行」和去重用 ROW_NUMBER,并列共享名次的排行榜用 RANK,「第 N 高的值」用 DENSE_RANK。
所有例子都基于 Nomto SQL 课程的航空公司数据库,使用 PostgreSQL 语法,运行在 Nomto SQL 练习场所用的 DuckDB 引擎上,每张结果表都是真实输出。OVER、窗口框架、累计求和和 LAG 的全貌见 SQL 窗口函数详解;本文只讲排名函数。
一张表看懂三者的区别
按飞行航段数给常旅客会员排名。有好几个会员的航段数相同,三个函数正是在这里分道扬镳:
WITH m AS (
SELECT tm.member_id, COUNT(*) AS flights
FROM ticket_members tm
JOIN ticket_flights tf ON tf.ticket_no = tm.ticket_no
GROUP BY tm.member_id
)
SELECT member_id, flights,
ROW_NUMBER() OVER (ORDER BY flights DESC, member_id) AS row_num,
RANK() OVER (ORDER BY flights DESC) AS rnk,
DENSE_RANK() OVER (ORDER BY flights DESC) AS dense_rnk
FROM m
ORDER BY row_num
LIMIT 11;
| member_id | flights | row_num | rnk | dense_rnk |
|---|---|---|---|---|
| 12589 | 33 | 1 | 1 | 1 |
| 2969 | 32 | 2 | 2 | 2 |
| 1659 | 31 | 3 | 3 | 3 |
| 8943 | 31 | 4 | 3 | 3 |
| 37704 | 31 | 5 | 3 | 3 |
| 39173 | 31 | 6 | 3 | 3 |
| 9128 | 30 | 7 | 7 | 4 |
| 15845 | 30 | 8 | 7 | 4 |
| 35285 | 30 | 9 | 7 | 4 |
| 38397 | 30 | 10 | 7 | 4 |
| 6702 | 29 | 11 | 11 | 5 |
逐行看:
- ROW_NUMBER 不管并列,从 1 数到 11。四个 31 个航段的会员分别得到 3、4、5、6,仅仅是因为
ORDER BY里有决胜列member_id。 - RANK 给四个人都排第 3,然后直接跳到 7:第 4、5、6 名被并列「占用」了。它的含义是「排在我前面的行数加一」。
- DENSE_RANK 也给他们排第 3,然后接着排 4。它给不同的值编号:33、32、31、30、29 分别是第 1 到第 5 个不同的航段数。
一句话记住:RANK 数的是行,DENSE_RANK 数的是不同的值,ROW_NUMBER 无视并列。
「前 5 名」各返回多少行
因为对并列的处理不同,同样是 <= 5,三个函数返回的行数不同:
WITH m AS (
SELECT tm.member_id, COUNT(*) AS flights
FROM ticket_members tm
JOIN ticket_flights tf ON tf.ticket_no = tm.ticket_no
GROUP BY tm.member_id
), r AS (
SELECT member_id, flights,
ROW_NUMBER() OVER (ORDER BY flights DESC, member_id) AS row_num,
RANK() OVER (ORDER BY flights DESC) AS rnk,
DENSE_RANK() OVER (ORDER BY flights DESC) AS dense_rnk
FROM m
)
SELECT COUNT(*) FILTER (WHERE row_num <= 5) AS by_row_number,
COUNT(*) FILTER (WHERE rnk <= 5) AS by_rank,
COUNT(*) FILTER (WHERE dense_rnk <= 5) AS by_dense_rank
FROM r;
| by_row_number | by_rank | by_dense_rank |
|---|---|---|
| 5 | 6 | 14 |
ROW_NUMBER <= 5永远恰好返回 5 行,而且悄悄丢掉了四个并列 31 的会员中的一个。RANK <= 5返回 6 行:名次在第 5 或更前的所有人,所以并列第 3 的四个会员全部入选。DENSE_RANK <= 5返回 14 行:航段数属于前五个值(33、32、31、30 或 29)的所有人。
三者都没有错,只是回答的问题不同。先弄清楚题目问的是哪个问题,再选函数。
FILTER (WHERE ...) 是 PostgreSQL 和 DuckDB 的写法;在 MySQL 或 Hive 里,改写成 SUM(CASE WHEN row_num <= 5 THEN 1 ELSE 0 END) 即可。
用 PARTITION BY 分组取前 N
加上 PARTITION BY,编号就在每个分组里重新开始。下面取两个机场各自航段数前 3 的会员(按常驻机场分组),三个函数并排:
WITH m AS (
SELECT lm.home_airport, tm.member_id, COUNT(*) AS flights
FROM ticket_members tm
JOIN ticket_flights tf ON tf.ticket_no = tm.ticket_no
JOIN loyalty_members lm ON lm.member_id = tm.member_id
WHERE lm.home_airport IN ('VKO', 'OVB')
GROUP BY lm.home_airport, tm.member_id
), r AS (
SELECT home_airport, member_id, flights,
ROW_NUMBER() OVER (PARTITION BY home_airport ORDER BY flights DESC, member_id) AS rn,
RANK() OVER (PARTITION BY home_airport ORDER BY flights DESC) AS rnk,
DENSE_RANK() OVER (PARTITION BY home_airport ORDER BY flights DESC) AS dense_rnk
FROM m
)
SELECT * FROM r
WHERE rnk <= 3
ORDER BY home_airport, rn;
| home_airport | member_id | flights | rn | rnk | dense_rnk |
|---|---|---|---|---|---|
| OVB | 39502 | 28 | 1 | 1 | 1 |
| OVB | 9835 | 25 | 2 | 2 | 2 |
| OVB | 4544 | 24 | 3 | 3 | 3 |
| OVB | 27427 | 24 | 4 | 3 | 3 |
| OVB | 38226 | 24 | 5 | 3 | 3 |
| OVB | 39050 | 24 | 6 | 3 | 3 |
| VKO | 39173 | 31 | 1 | 1 | 1 |
| VKO | 6702 | 29 | 2 | 2 | 2 |
| VKO | 17160 | 29 | 3 | 2 | 2 |
| VKO | 32434 | 29 | 4 | 2 | 2 |
按 rnk <= 3 筛选,新西伯利亚(OVB)返回六行(四人并列第 3),伏努科沃(VKO)返回四行(三人并列第 2)。如果用 rn <= 3,每个机场恰好三行,而并列的会员里谁能入选只取决于决胜列。发奖名单这样做不公平;但如果只是「每个机场展示三个例子」的小组件,这正是你要的。这和力扣上的「部门工资前三高的所有员工」是同一类题,那道题要求并列的都算,所以要用 DENSE_RANK(按不同工资取前三),而不是 ROW_NUMBER。
所有分组取前 N 的查询都适用两条规则:
- 在外层筛选。
WHERE rnk <= 3不能和计算rnk的SELECT写在同一层,因为WHERE在窗口函数之前执行。先在 CTE 或子查询里算出名次,再在外层筛选。DuckDB、Snowflake、BigQuery 和 Databricks 还支持QUALIFY rnk <= 3;PostgreSQL、MySQL、SQL Server 不支持。 - 按分组分区,按指标排序。
PARTITION BY home_airport ORDER BY flights DESC的意思是「在每个机场内部,按航段数」。忘了PARTITION BY,就成了全表排名。
去重:用 ROW_NUMBER 每个键只留一行
去重时,并列正是关键:重复行在所有列上都并列,而你只想留一行。这是 ROW_NUMBER 的活,绝不能用 RANK,否则两份副本都会编成 1、都被保留。
app_events 表里有些事件收到了两次:同一个客户端、同一个事件、同一个 event_time,只有 received_at 不同。给副本编号,最早到达的排第一:
SELECT event_id, event_name, event_time, received_at,
ROW_NUMBER() OVER (PARTITION BY client_id, event_name, event_time
ORDER BY received_at, event_id) AS rn
FROM app_events
WHERE client_id = '000034d98b23' AND event_time < '2026-07-05 02:00'
ORDER BY event_time, rn;
| event_id | event_name | event_time | received_at | rn |
|---|---|---|---|---|
| 131068 | app_open | 2026-07-05 01:02:07 | 2026-07-05 01:02:07 | 1 |
| 131081 | search | 2026-07-05 01:02:31 | 2026-07-05 01:02:31 | 1 |
| 131116 | search | 2026-07-05 01:02:31 | 2026-07-05 01:03:56 | 2 |
| 131085 | view_offer | 2026-07-05 01:02:41 | 2026-07-05 01:02:42 | 1 |
| 131096 | select_fare | 2026-07-05 01:03:32 | 2026-07-05 01:03:34 | 1 |
search 事件到达了两次,相隔 85 秒;副本得到 rn = 2。在全表上只保留 rn = 1:
WITH ranked AS (
SELECT ROW_NUMBER() OVER (PARTITION BY client_id, event_name, event_time
ORDER BY received_at, event_id) AS rn
FROM app_events
)
SELECT COUNT(*) AS all_rows,
COUNT(*) FILTER (WHERE rn = 1) AS kept,
COUNT(*) FILTER (WHERE rn > 1) AS duplicates
FROM ranked;
| all_rows | kept | duplicates |
|---|---|---|
| 1494723 | 1484034 | 10689 |
PARTITION BY 的列定义了什么叫「同一行」;ORDER BY 决定留下哪一份(这里是最早收到的;改成 ORDER BY received_at DESC 就留最新的)。最后加一个唯一列,比如 event_id,让选择可复现。查找和删除重复数据的其他方法,见 SQL 查找重复数据与删除重复行。
第 N 高的值:用 DENSE_RANK
「查询第二高的薪水」是力扣上最有名的 SQL 题之一,也是面试常考题,而 RANK 恰恰会在这里悄悄出错。很多机票的价格相同:
WITH p AS (
SELECT amount,
RANK() OVER (ORDER BY amount DESC) AS rnk,
DENSE_RANK() OVER (ORDER BY amount DESC) AS dense_rnk
FROM ticket_flights
)
SELECT amount, MIN(rnk) AS rnk, MIN(dense_rnk) AS dense_rnk, COUNT(*) AS tickets
FROM p
WHERE dense_rnk <= 4
GROUP BY amount
ORDER BY amount DESC;
| amount | rnk | dense_rnk | tickets |
|---|---|---|---|
| 2033.00 | 1 | 1 | 427 |
| 2013.00 | 428 | 2 | 70 |
| 1998.00 | 498 | 3 | 691 |
| 1993.00 | 1189 | 4 | 856 |
有 427 张机票是最高票价 2,033 美元,所以 RANK 给下一个价格 2,013 美元的名次是 428。WHERE rnk = 2 返回零行;WHERE dense_rnk = 2 返回 2,013 美元,这才是正确答案。ROW_NUMBER() = 2 会再次返回 2,033 美元,因为第二行只是另一张最高价的机票。
WITH p AS (
SELECT amount, DENSE_RANK() OVER (ORDER BY amount DESC) AS dense_rnk
FROM ticket_flights
)
SELECT DISTINCT amount FROM p WHERE dense_rnk = 2;
| amount |
|---|
| 2013.00 |
力扣的「分数排名」也是同一个点:要求并列同名次、名次连续不跳号,答案就是 DENSE_RANK。更多这类题目见 SQL 面试题。
到底用哪个
| 任务 | 函数 | 原因 |
|---|---|---|
| 恰好 N 行、每组固定 N 行 | ROW_NUMBER |
编号唯一;并列由决胜列切开 |
| 去重、每个键取最新一行 | ROW_NUMBER |
副本并列,而你只要 rn = 1 的那一行 |
| 按稳定顺序分页 | ROW_NUMBER |
每行都有自己的位置 |
| 排行榜、比赛名次 | RANK |
并列共享名次,下一名跳号(1、2、2、4) |
| 前 N 名且包括在第 N 名并列的所有人 | RANK |
rnk <= N 保留所有并列第 N 的行 |
| 第 N 高或第 N 低的值 | DENSE_RANK |
数的是不同的值,没有空号 |
| 前 N 个不同的值(三个最高价) | DENSE_RANK |
dense_rnk <= 3 返回属于这些值的所有行 |
还有一条规则:用 ROW_NUMBER 时,ORDER BY 最后一定要加一个唯一列。 如果排序有并列,数据库下次运行时可能给并列行不同的编号,你的「前 5 名」或去重后的表就会在数据没变的情况下发生变化。在 Hive、Spark SQL 这类分布式引擎上,这种不确定性更常见。RANK 和 DENSE_RANK 不需要这样做,因为并列行本来就同号。
NTILE、PERCENT_RANK 和 CUME_DIST
另外三个排名函数也值得了解:
NTILE(n)把排好序的行分成n个大小(几乎)相同的桶:四分位、十分位。PERCENT_RANK()等于(rank - 1) / (rows - 1):第一行是 0,最后一行是 1。CUME_DIST()是取值小于等于当前行的行所占的比例。
以九种机型按航程排序为例:
SELECT model, range,
NTILE(4) OVER (ORDER BY range DESC) AS quartile,
ROUND(PERCENT_RANK() OVER (ORDER BY range), 3) AS pct_rank,
ROUND(CUME_DIST() OVER (ORDER BY range), 3) AS cume_dist
FROM aircrafts
ORDER BY range DESC;
| model | range | quartile | pct_rank | cume_dist |
|---|---|---|---|---|
| Boeing 777-300 | 11100 | 1 | 1 | 1 |
| Boeing 767-300 | 7900 | 1 | 0.875 | 0.889 |
| Airbus A319-100 | 6700 | 1 | 0.75 | 0.778 |
| Airbus A320-200 | 5700 | 2 | 0.625 | 0.667 |
| Airbus A321-200 | 5600 | 2 | 0.5 | 0.556 |
| Boeing 737-300 | 4200 | 3 | 0.375 | 0.444 |
| Sukhoi Superjet-100 | 3000 | 3 | 0.25 | 0.333 |
| Bombardier CRJ-200 | 2700 | 4 | 0.125 | 0.222 |
| Cessna 208 Caravan | 1200 | 4 | 0 | 0.111 |
九行没法平均分成四份,所以第一个桶多分到一行(3、2、2、2)。
NTILE 和 ROW_NUMBER 有同样的盲点:它无视并列。把所有会员按航段数分成十分位:
WITH m AS (
SELECT tm.member_id, COUNT(*) AS flights
FROM ticket_members tm
JOIN ticket_flights tf ON tf.ticket_no = tm.ticket_no
GROUP BY tm.member_id
), t AS (
SELECT member_id, flights,
NTILE(10) OVER (ORDER BY flights DESC, member_id) AS decile
FROM m
)
SELECT decile, COUNT(*) AS members, MIN(flights) AS min_flights, MAX(flights) AS max_flights
FROM t
GROUP BY decile
ORDER BY decile;
| decile | members | min_flights | max_flights |
|---|---|---|---|
| 1 | 4158 | 8 | 33 |
| 2 | 4158 | 6 | 8 |
| 3 | 4158 | 6 | 6 |
| 4 | 4158 | 4 | 6 |
| 5 | 4157 | 4 | 4 |
| 6 | 4157 | 4 | 4 |
| 7 | 4157 | 3 | 4 |
| 8 | 4157 | 2 | 3 |
| 9 | 4157 | 2 | 2 |
| 10 | 4157 | 1 | 2 |
每个桶大小相同,但 8 个航段的会员同时出现在第 1 和第 2 个十分位,4 个航段的会员分散在四个十分位里。如果「前 10%」不能把相同的值拆开,就改用 PERCENT_RANK() <= 0.1 或 CUME_DIST(),它们给并列行相同的值。
各数据库的差异
ROW_NUMBER、RANK、DENSE_RANK、NTILE、PERCENT_RANK 和 CUME_DIST 都是标准 SQL,在 PostgreSQL、DuckDB、SQL Server(后两个从 2012 版起)、Oracle、BigQuery、Snowflake 里用法相同,Hive 和 Spark SQL 也都支持。MySQL 从 8.0 开始支持;MySQL 5.7 完全没有窗口函数,很多老系统还停留在这个版本,面试里也常被问到「没有窗口函数怎么排名」(答案通常是用变量或关联子查询)。SQL Server 要求所有排名函数的 OVER 里都必须有 ORDER BY,ROW_NUMBER 也不例外;PostgreSQL 和 DuckDB 接受 ROW_NUMBER() OVER (),但那样编号是随机的。
常见问题
ROW_NUMBER、RANK 和 DENSE_RANK 有什么区别?
只在并列时不同。ROW_NUMBER 给每一行唯一的编号(1、2、3、4)。RANK 让并列的行同号,并跳过后面的号(1、2、2、4)。DENSE_RANK 让并列的行同号,但不跳号(1、2、2、3)。
分组取前 N 该用哪个?
如果每组必须恰好返回 N 行,用 ROW_NUMBER。如果在第 N 名并列的人都要算进去,用 RANK,这样每组可能返回超过 N 行。两种情况都要先在 CTE 里算出编号,再在外层筛选。
RANK 为什么会跳号?
因为 RANK 等于排在当前行前面的行数加一。如果四行并列第 3,下一行前面有六行,所以它是第 7。DENSE_RANK 数的是不同的值,所以下一行是第 4。
SQL 怎么取第二高的值?
用 DENSE_RANK() OVER (ORDER BY value DESC),保留 dense_rnk = 2。最高值被多行共享时,RANK 可能什么也返回不了,ROW_NUMBER 可能再次返回最高值。
ROW_NUMBER 的结果是确定的吗?
只有当 OVER 里的 ORDER BY 唯一时才是。排序中有并列时,数据库每次运行可能给并列行不同的编号。在最后加一个唯一列(比如 id)作为排序键。
其他语言: English, Español, Português (Brasil), Tiếng Việt