Quick answer: what does ROW_NUMBER do in SQL? 快速答案:SQL 中的 ROW_NUMBER 是做什么的?
ROW_NUMBER assigns a unique sequential integer to every row
inside a window partition, using the order defined inside
OVER(...)
.
Numbering begins at 1 and restarts for every partition. Unlike
RANK
and
DENSE_RANK
, tied rows still receive different numbers.
ROW_NUMBER 会按照
OVER(...)
中定义的顺序,为每个窗口分区内的每一行分配唯一且连续的整数。
编号从 1 开始,并在每个分区重新开始。与
RANK
和
DENSE_RANK
不同,即使排序值并列,每行仍会获得不同编号。
The function does not permanently number table rows and does not guarantee final display order. It calculates a value for the current result set. Use it when you need one winner per business key, the first few rows per group, numbered slices for a report, or an explicit row sequence for downstream logic.
该函数不会永久修改表中的行号,也不会保证最终展示顺序。它只为当前结果集计算一个临时值。需要为每个业务键选择唯一结果、提取每组前几条记录、生成报表分页区间或为后续逻辑提供明确顺序时,都可以使用它。
ROW_NUMBER() OVER (
PARTITION BY group_column
ORDER BY sort_column DESC, unique_id
) AS row_num
Understand the ROW_NUMBER syntax before using it 使用 ROW_NUMBER 前先理解语法
Four decisions control the result. The input rowset decides what
can be numbered.
PARTITION BY
decides where numbering restarts. The window
ORDER BY
decides which row comes first. A tie-breaker decides whether
that order is reproducible. Leaving any decision implicit can
produce a query that runs successfully but answers the wrong
question.
结果由四个决定共同控制:输入行集决定哪些记录参与编号,
PARTITION BY
决定编号从哪里重新开始,窗口内的
ORDER BY
决定谁排在前面,而并列破局字段决定结果是否能够稳定复现。任何一项含糊,都可能让查询“能运行但答案错误”。
Joins, filters, and grouping define the rows visible to the window function. A one-to-many join may duplicate candidates before ranking begins.
连接、过滤与聚合共同定义窗口函数能看到的记录。一对多连接可能在排名之前就复制候选行。
Use the business boundary that should produce one independent sequence: customer, product, account, session, or a composite key.
选择应形成独立编号序列的业务边界,例如客户、产品、账户、会话或复合键。
State the business priority explicitly: newest timestamp, highest score, lowest cost, or another measurable rule.
明确业务优先级,例如最新时间、最高得分、最低成本或其他可度量标准。
Append a stable unique column so two otherwise equal rows never compete in an unspecified order.
追加稳定且唯一的字段,避免其他排序值相同时由数据库任意决定顺序。
Logical evaluation order matters.
Window functions are evaluated after the same-level
WHERE
,
GROUP BY
, and
HAVING
. Compute
ROW_NUMBER
inside a CTE or subquery, then filter its alias outside. The
official
PostgreSQL window-function tutorial
demonstrates this pattern.
逻辑执行顺序很重要。
窗口函数在同一层的
WHERE
、
GROUP BY
与
HAVING
之后计算。因此应先在 CTE 或子查询中计算
ROW_NUMBER
,再到外层过滤别名。PostgreSQL
官方窗口函数教程也展示了这种写法。
Make ROW_NUMBER deterministic when ties exist 存在并列值时让 ROW_NUMBER 保持确定性
Suppose two status updates for the same ticket share the same
updated_at
value. If the window orders only by that timestamp, either
update may receive row number 1. The result can change after a
new execution plan, parallel processing, a maintenance
operation, or simply a different physical row order. The SQL is
valid, but the winner is not defined.
假设同一工单的两次状态更新具有相同的
updated_at
。如果窗口只按时间戳排序,任意一条都可能得到编号
1。执行计划变化、并行处理、维护操作或物理行顺序变化,都可能改变结果。SQL
语法没有错,但“获胜者”没有被定义。
-- Unstable when updated_at contains ties
ROW_NUMBER() OVER (
PARTITION BY ticket_id
ORDER BY updated_at DESC
)
-- Stable if event_id is unique and immutable
ROW_NUMBER() OVER (
PARTITION BY ticket_id
ORDER BY updated_at DESC, event_id DESC
)
A good tie-breaker is non-null, stable, and unique within the
partition. A primary key is often suitable. Do not append a
random function merely to silence ties; that explicitly makes
selection variable. Also remember that the window ordering
controls numbering, not final presentation. Add a query-level
ORDER BY
when consumers need the output itself sorted.
理想的破局字段应当非空、稳定,并且在分区内唯一。主键通常是合适选择。不要为了消除并列而加入随机函数,那会主动制造不稳定性。同时要记住:窗口内排序只控制编号,不控制最终展示顺序;如果下游要求结果排序,还应在查询末尾增加外层
ORDER BY
。
Test the tie case intentionally. Add at least two rows with the same primary business sort value, run the query repeatedly, and confirm that the chosen winner matches a documented rule—not an accidental physical order.
请主动测试并列情况。 至少创建两条主要业务排序值相同的记录,多次执行查询,确认胜出记录符合已记录的规则,而不是依赖偶然的物理顺序。
ROW_NUMBER vs RANK vs DENSE_RANK ROW_NUMBER、RANK 与 DENSE_RANK 的区别
Choose the function according to the meaning of a tie. For
values 100, 90, 90, and 80 ordered descending,
ROW_NUMBER
produces 1, 2, 3, 4;
RANK
produces 1, 2, 2, 4; and
DENSE_RANK
produces 1, 2, 2, 3. The functions are not interchangeable when
a boundary such as “top three” is involved.
应根据并列的业务含义选择函数。对按降序排列的 100、90、90、80,
ROW_NUMBER
产生 1、2、3、4;
RANK
产生 1、2、2、4;
DENSE_RANK
产生 1、2、2、3。当存在“前三名”等边界时,它们不能互换。
| Function 函数 | Tie behavior 并列行为 | Best fit 适用场景 | Boundary risk 边界风险 |
|---|---|---|---|
ROW_NUMBER
|
Every row gets a different number 每行编号不同 | One winner, fixed row count, pagination 唯一获胜者、固定行数、分页 | Needs an explicit tie-breaker 必须明确破局规则 |
RANK
|
Peers tie; later ranks contain gaps 并列同名次,后续名次有空缺 | Competition-style ranking 竞赛式排名 | Top N may return more than N rows Top N 可能超过 N 行 |
DENSE_RANK
|
Peers tie; no gaps 并列同名次,无空缺 | Distinct value tiers 不同数值层级 | Top N tiers may return many rows 前 N 层可能返回很多行 |
The
PostgreSQL reference
defines
ROW_NUMBER
as the current row's number within its partition and
distinguishes it from peer-aware ranking. Use
ROW_NUMBER
when the requirement is exactly N physical rows or one selected
row. Use a peer-aware function when equal business values must
share rank.
PostgreSQL 官方参考将
ROW_NUMBER
定义为当前行在分区内的编号,并将其与考虑并列关系的排名函数区分开来。业务要求“恰好
N 条物理记录”或“只保留一条”时使用
ROW_NUMBER
;业务上相等的值必须共享名次时,则使用考虑并列的函数。
A complete ROW_NUMBER SQL example 一个完整的 ROW_NUMBER SQL 示例
Assume
orders
contains one row per order. The report needs each customer's
orders from newest to oldest and must behave predictably when
two orders share a timestamp. The combination of
order_date
and unique
order_id
defines the sequence.
假设
orders
每个订单一行。报表需要按从新到旧展示每位客户的订单,并且在两个订单时间相同时仍保持稳定。此时由
order_date
与唯一的
order_id
共同定义顺序。
SELECT
order_id,
customer_id,
order_date,
amount,
ROW_NUMBER() OVER (
PARTITION BY customer_id
ORDER BY order_date DESC, order_id DESC
) AS customer_order_number
FROM orders
WHERE order_status = 'completed'
ORDER BY customer_id, customer_order_number;
The
WHERE
clause removes non-completed orders before numbering, so the
sequence describes completed orders only.
PARTITION BY customer_id
restarts at 1 for each customer. The two ordering columns make
the order total. Finally, the outer
ORDER BY
makes the delivered result readable; it does not change the row
numbers already calculated.
WHERE
会在编号前排除未完成订单,因此序列只描述已完成订单。
PARTITION BY customer_id
让每位客户重新从 1 开始。两个排序字段共同形成完全顺序,最外层
ORDER BY
仅让结果便于阅读,不会改变已经计算出的编号。
Return the top N rows per group 使用 ROW_NUMBER 返回每组 Top N
A global
LIMIT
returns N rows for the entire result. To return exactly N rows
for every customer, product, or region, calculate a partitioned
row number first and filter it outside. This is one of the
clearest reasons to use a window function.
全局
LIMIT
只会为整个结果返回 N 行。如果需要为每个客户、产品或地区恰好返回
N
行,应先计算分区编号,再在外层过滤。这是使用窗口函数最典型的理由之一。
WITH ranked_orders AS (
SELECT
o.*,
ROW_NUMBER() OVER (
PARTITION BY customer_id
ORDER BY amount DESC, order_id
) AS rn
FROM orders o
WHERE order_status = 'completed'
)
SELECT *
FROM ranked_orders
WHERE rn <= 3;
This returns a maximum of three rows per customer. It
deliberately breaks equal amounts using
order_id
. If the requirement says “include every order tied for
third-highest amount,” use
RANK
or
DENSE_RANK
and accept that a customer may return more than three rows.
Document that distinction because both interpretations are
plausible.
该查询每位客户最多返回三行,并用
order_id
打破金额并列。如果业务要求“包含所有与第三高金额并列的订单”,则应使用
RANK
或
DENSE_RANK
,并接受每位客户可能返回超过三行。两种解释都合理,因此必须记录清楚。
Deduplicate rows and keep the intended winner 去重并保留业务上正确的记录
“Remove duplicates” is incomplete as a requirement. You must
define the duplicate key and the winner. For a customer profile
history table, the key might be
customer_id
; the winner might be the greatest
updated_at
, followed by the greatest ingestion identifier when timestamps
tie.
ROW_NUMBER
expresses that policy directly.
“删除重复项”并不是完整需求。必须同时定义重复键与获胜规则。对于客户资料历史表,重复键可能是
customer_id
;胜出记录可能是
updated_at
最大者,时间相同时再选择摄取标识最大的记录。
ROW_NUMBER
可以直接表达这一政策。
WITH candidates AS (
SELECT
p.*,
ROW_NUMBER() OVER (
PARTITION BY customer_id
ORDER BY updated_at DESC, ingestion_id DESC
) AS rn
FROM customer_profile_history p
)
SELECT *
FROM candidates
WHERE rn = 1;
Do not use this pattern to hide unexplained duplication introduced by an incorrect join. First measure the row count and key uniqueness before and after each join. If one order becomes five rows because it matches five line items, ranking one row does not repair the grain—it merely discards information according to a possibly unrelated order.
不要用这种模式掩盖错误连接造成的重复。应先测量每次连接前后的行数与键唯一性。如果一个订单因为匹配五条明细而变成五行,排名后只留一行并没有修复粒度,只是按照可能无关的顺序丢弃信息。
- Define the duplicate key independently of the winner rule.
- Make the winner rule deterministic and explain why it matches the business.
- Count partitions with more than one candidate before discarding rows.
- Retain rejected records or audit counts when deletion has governance impact.
- 分别定义重复键和获胜规则,不要把两者混为一谈。
- 让获胜规则具有确定性,并解释为何符合业务。
- 丢弃记录前,统计候选行超过一条的分区数量。
- 当删除影响治理时,保留被淘汰记录或审计计数。
Use ROW_NUMBER for pagination with clear tradeoffs 使用 ROW_NUMBER 分页并理解取舍
Numbered pagination can return a requested range and is useful for reports that must jump directly to a page. The ordering must be stable, and the snapshot must be consistent if users expect page boundaries not to move while data changes.
编号分页可以返回指定区间,适合必须直接跳转到某页的报表。排序必须稳定;如果用户希望数据变化期间分页边界不移动,还需要一致的读取快照。
WITH numbered AS (
SELECT
order_id,
order_date,
customer_id,
amount,
ROW_NUMBER() OVER (
ORDER BY order_date DESC, order_id DESC
) AS rn
FROM orders
)
SELECT *
FROM numbered
WHERE rn BETWEEN 101 AND 125
ORDER BY rn;
The apparent simplicity can hide cost: the engine may sort and number every qualifying row before it returns a small page. For deep, frequently requested pages on large changing tables, keyset pagination often performs more predictably by requesting rows after the last seen ordering key. Keyset pagination cannot jump to an arbitrary page as naturally, so the choice depends on product behavior, not syntax preference.
这种写法表面简单,但可能隐藏较大成本:数据库可能先排序并编号所有符合条件的行,最后才返回一个很小的页面。对于频繁访问的大型动态表,键集分页通常更稳定,因为它直接请求上次排序键之后的记录。但键集分页不适合自然跳转到任意页,因此选择应由产品行为决定,而非语法偏好。
| Approach 方案 | Strength 优势 | Main limitation 主要限制 |
|---|---|---|
ROW_NUMBER
range
|
Explicit row ranges and page jumps 明确行区间,可跳页 | May number a large intermediate result 可能编号庞大中间结果 |
| OFFSET / FETCH OFFSET / FETCH | Concise standard page interface 分页接口简洁 | Deep offsets can scan or discard many rows 深分页可能扫描并丢弃大量行 |
| Keyset pagination 键集分页 | Stable continuation with suitable indexes 配合索引可稳定续页 | No natural arbitrary page jump 不便直接跳到任意页 |
Place filters and joins at the correct grain 在正确粒度放置过滤与连接
The same
ROW_NUMBER
expression can answer different questions depending on filter
placement. Filtering to completed orders before numbering means
“latest completed order.” Numbering all orders and filtering
status afterward means “latest order, but return it only if
completed.” Those results diverge whenever a customer's newest
order is pending.
同一个
ROW_NUMBER
表达式会因过滤位置不同而回答不同问题。先过滤已完成订单再编号,表示“最新的已完成订单”;先为全部订单编号、外层再过滤状态,表示“最新订单,并且仅当它已完成时返回”。当客户最新订单仍待处理时,两者结果完全不同。
- State the decision. Write whether eligibility is determined before ranking or whether rank is assigned across the full population.
- Establish the row grain. Confirm what one row represents before any one-to-many join.
- Apply eligibility filters. Put filters inside or outside the ranked CTE according to the stated decision.
- Join attributes deliberately. If possible, rank to one row per key before joining to dimensions that should not multiply rows.
- Recount and test. Validate row counts, distinct keys, ties, null keys, and missing relationships.
- 明确决策。 记录资格条件是在排名前确定,还是先对完整总体排名。
- 确定行粒度。 在任何一对多连接前,确认一行代表什么实体。
- 放置资格过滤。 根据决策,把过滤条件放在排名 CTE 内部或外部。
- 谨慎连接属性。 如果可以,先把每个键排名到一行,再连接不应增加行数的维表。
- 重新计数与测试。 检查行数、唯一键、并列、空键和缺失关系。
Recognize when ROW_NUMBER adds query complexity 识别 ROW_NUMBER 带来的查询复杂度
ROW_NUMBER
itself is compact. Complexity comes from the work needed to
produce the correct ordered partitions and from the layers built
around the function. Large sorts, skewed partitions, repeated
windows, wide rows, nested CTEs, and joins that expand the input
can make a short-looking expression expensive or difficult to
verify.
ROW_NUMBER
本身很短。真正的复杂度来自生成正确有序分区所需的工作,以及函数周围叠加的查询层。大规模排序、分区倾斜、重复窗口、宽行、嵌套
CTE
和扩张输入的连接,都可能让看似简短的表达式代价高昂且难以验证。
| Complexity signal 复杂度信号 | Why it matters 为什么重要 | Review question 检查问题 |
|---|---|---|
| Large global partition 大型全局分区 | All qualifying rows may require one ordered operation 全部候选行可能需要一次整体排序 | Can selective filtering occur earlier? 能否更早选择性过滤? |
| Skewed partition key 分区键倾斜 | One customer or tenant may dominate memory and runtime 单个客户或租户可能主导内存与耗时 | What is the largest partition? 最大分区有多大? |
| Different window orderings 多个不同窗口排序 | The engine may need multiple sorts 数据库可能需要多次排序 | Can windows share a definition? 窗口能否共享定义? |
| Wide input rows 输入行过宽 | Sorting carries unnecessary columns 排序时携带无关字段 | Can projection be narrowed before ranking? 能否在排名前缩窄字段? |
| One-to-many joins first 先做一对多连接 | More rows are sorted and semantic grain may be lost 排序行数增加且语义粒度可能丢失 | Can ranking happen before expansion? 能否在扩张前排名? |
| Nested ranking layers 嵌套排名层 | Correctness becomes difficult to trace 正确性难以追踪 | Does each layer have a named grain? 每层是否有明确粒度? |
Indexes that begin with selective filter columns and continue with partition and ordering columns may reduce work in some databases and plans, but no universal index recipe exists. Inspect the execution plan with realistic data. Check sort operations, memory spills, estimated versus actual rows, partition skew, and whether an early filter or narrower projection changes the plan safely.
某些数据库和执行计划可以利用以选择性过滤字段开头、随后包含分区与排序字段的索引来减少工作,但不存在通用索引公式。应使用真实规模数据检查执行计划,关注排序、内存溢写、估算与实际行数、分区倾斜,以及提前过滤或缩窄字段是否能安全改变计划。
Use structural review before engine tuning. The InfiniSynapse SQL Complexity Checker can help identify window functions, nested layers, joins, repeated logic, and other structural signals in the complete statement. It does not replace an execution plan or database-specific benchmarking; use it to decide where deeper review is most valuable.
先做结构审查,再做引擎调优。 InfiniSynapse SQL Complexity Checker 可帮助识别完整语句中的窗口函数、嵌套层、连接、重复逻辑及其他结构信号。它不能替代执行计划或数据库专属基准测试,但能帮助确定最值得深入检查的位置。
Check ROW_NUMBER behavior across SQL dialects 检查不同 SQL 方言中的 ROW_NUMBER
PostgreSQL, SQL Server, MySQL 8+, Oracle, Snowflake, BigQuery,
and other modern platforms support
ROW_NUMBER
, but surrounding syntax and optimizer behavior differ. SQL
Server requires an
ORDER BY
in the window specification. MySQL documents that omitting it
makes numbering nondeterministic. Some cloud warehouses support
QUALIFY
, which filters window results without an extra visible CTE;
PostgreSQL, SQL Server, and MySQL commonly use a subquery or CTE
instead.
PostgreSQL、SQL Server、MySQL 8+、Oracle、Snowflake、BigQuery
等现代平台均支持
ROW_NUMBER
,但周边语法与优化器行为不同。SQL Server 要求窗口定义中包含
ORDER BY
;MySQL 文档说明省略排序会使编号不确定。部分云数据仓库支持
QUALIFY
,可以不增加可见 CTE 就过滤窗口结果;PostgreSQL、SQL Server 与
MySQL 通常使用子查询或 CTE。
| Dialect family 方言类型 | Typical filtering pattern 典型过滤方式 | Review note 检查要点 |
|---|---|---|
| PostgreSQL | CTE or subquery CTE 或子查询 | Tied ordering rows have unspecified order without a tie-breaker 没有破局字段时,并列行顺序未指定 |
| SQL Server | CTE or subquery CTE 或子查询 | Window ORDER BY is required; numbering is temporary 窗口排序必需;编号是临时值 |
| MySQL 8+ | CTE or subquery CTE 或子查询 | ORDER BY determines numbering; without it numbering is nondeterministic ORDER BY 决定编号;省略时不确定 |
| Warehouses with QUALIFY 支持 QUALIFY 的数仓 |
QUALIFY ROW_NUMBER() ... = 1
|
Confirm platform syntax and portability requirements 确认平台语法与可移植性要求 |
For authoritative syntax, consult the Microsoft SQL Server ROW_NUMBER documentation and the MySQL window-function reference . Test the exact production engine and version rather than assuming a portable query has identical performance everywhere.
权威语法可参考 Microsoft SQL Server ROW_NUMBER 文档与 MySQL 窗口函数参考。即使查询语法可移植,也不应假设各平台性能相同;请在实际生产引擎和版本上测试。
Validate a ROW_NUMBER query before production 上线前验证 ROW_NUMBER 查询
Correct sample output is not enough. A ranking query should survive adversarial data, changes in volume, and a second reviewer who can reconstruct the business rule from the SQL.
样例输出正确并不足够。排名查询必须能够应对极端数据、数据量变化,并让第二位审查者可以从 SQL 还原业务规则。
- Name the grain. Write what one input row and one output row represent.
- State eligibility. Decide which filters apply before ranking and which conditions apply after it.
- Test partitions. Include empty relationships, one-row groups, large groups, null partition keys, and skewed tenants.
- Force ties. Create duplicate primary sort values and confirm the unique tie-breaker selects the intended row.
- Check boundaries. Test N-1, N, and N+1 candidates for top-N logic and the first/last rows of every page.
- Audit joins. Compare total rows and distinct business keys before and after each join.
- Inspect the plan. Use representative volume to review sorts, spills, row estimates, and repeated window work.
- Review the complete SQL. Evaluate the function together with CTEs, joins, projections, filters, and downstream assumptions.
- 命名粒度。 写明一条输入记录和一条输出记录分别代表什么。
- 声明资格。 区分排名前过滤条件和排名后筛选条件。
- 测试分区。 覆盖空关系、单行分组、大分组、空分区键与租户倾斜。
- 制造并列。 创建主要排序值重复的数据,确认唯一破局字段选中预期记录。
- 检查边界。 对 Top-N 测试 N-1、N、N+1 条候选,并测试每页首尾记录。
- 审计连接。 比较每次连接前后的总行数与不同业务键数量。
- 检查执行计划。 用代表性数据量查看排序、溢写、行数估算与重复窗口工作。
- 审查完整 SQL。 将函数与 CTE、连接、字段投影、过滤和下游假设放在一起评估。
Inspect the complete ROW_NUMBER query 检查完整的 ROW_NUMBER 查询
A window expression can be correct while the surrounding joins, nested CTEs, repeated windows, and projections make the statement difficult to review. Paste a sanitized version of the complete query into the InfiniSynapse SQL Complexity Checker to identify structural hotspots before engine-level testing.
窗口表达式可能完全正确,但周围的连接、嵌套 CTE、重复窗口和字段投影仍会让整条语句难以审查。将脱敏后的完整查询粘贴到 InfiniSynapse SQL Complexity Checker,先定位结构热点,再进行数据库层面的执行计划测试。
Open SQL Complexity Checker 打开 SQL 复杂度检查器 Use sanitized SQL. Never paste credentials, secrets, personal data, or sensitive literal values. 请使用脱敏 SQL,切勿粘贴凭据、密钥、个人数据或敏感字面值。ROW_NUMBER SQL frequently asked questions ROW_NUMBER SQL 常见问题
It assigns a unique sequential integer to each row within a window partition according to the window ordering. Numbering restarts at 1 in each partition.
它按照窗口排序为每个分区中的每一行分配唯一连续整数,并在每个分区从 1 重新开始。
No. It labels every row. Deduplication occurs only when an
outer query filters to one numbered row per duplicate key,
usually
rn = 1
. The ordering must define the intended winner.
不会。它只为每行编号。只有外层查询按重复键过滤到一行(通常为
rn = 1
)时才实现去重,而且排序必须定义正确获胜者。
The window ordering is not unique. Add a stable tie-breaker so the combination of partition and ordering columns uniquely determines every row.
因为窗口排序不唯一。追加稳定的破局字段,让分区字段与排序字段的组合能够唯一确定每一行。
Usually not at the same query level. Compute the value in a
CTE or subquery and filter it outside. Platforms supporting
QUALIFY
can filter window results there.
同一查询层通常不可以。应在 CTE
或子查询中计算,再到外层过滤。支持
QUALIFY
的平台可以直接在该子句中过滤窗口结果。
Do not choose by assumed speed. Both depend heavily on ordered partitions. Choose the function with the correct tie semantics, then benchmark the real query and inspect its plan.
不要根据假设的速度选择。二者都高度依赖有序分区。先选择并列语义正确的函数,再对真实查询进行基准测试并检查执行计划。
Official sources and ROW_NUMBER documentation ROW_NUMBER 官方来源与参考资料
These primary sources support the syntax and behavior described above. Database behavior can change across versions, so confirm the documentation for your deployed engine.
以下一手资料支持本文的语法与行为说明。数据库行为可能随版本变化,请同时确认生产环境所用引擎版本的文档。