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

逐行看:

一句话记住: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

三者都没有错,只是回答的问题不同。先弄清楚题目问的是哪个问题,再选函数。

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 的查询都适用两条规则:

  1. 在外层筛选。 WHERE rnk <= 3 不能和计算 rnk 的 SELECT 写在同一层,因为 WHERE 在窗口函数之前执行。先在 CTE 或子查询里算出名次,再在外层筛选。DuckDB、Snowflake、BigQuery 和 Databricks 还支持 QUALIFY rnk <= 3;PostgreSQL、MySQL、SQL Server 不支持。
  2. 按分组分区,按指标排序。 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

另外三个排名函数也值得了解:

以九种机型按航程排序为例:

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