在数据处理和报表开发中,经常会遇到需要将多张表、多个数据源的记录合并后编一个连续且不重复的序号。传统自增主键只能保证单表内唯一,一旦做UNION或跨库查询就会撞号。SQL标准里的窗口函数ROW_NUMBER可以纯查询层解决这一问题,它按照你给定的顺序为结果集的每一行计算出一个从1开始的递增整数,天然具备全局唯一性。

一、ROW_NUMBER基本语法与原理
ROW_NUMBER是SQL:2003引入的窗口函数之一,它不为数据做聚合,而是为每一行计算一个行号。其核心语法是在SELECT列表里写ROW_NUMBER() OVER (ORDER BY 排序列),数据库会先按ORDER BY对结果集排序,再依次编上1、2、3……的序号。
与RANK、DENSE_RANK不同,ROW_NUMBER即使遇到相同的排序值也一定会给出不同的数字,因此它是生成全局唯一自增序列最稳妥的选择。下面的示例演示了如何从两张用户表中取数并编全局号:
-- 从两个地区用户表合并后生成全局唯一序号 SELECT ROW_NUMBER() OVER (ORDER BY u.create_time, u.user_id) AS global_seq, u.user_id, u.user_name, u.create_time FROM ( SELECT user_id, user_name, create_time FROM user_east UNION ALL SELECT user_id, user_name, create_time FROM user_west ) u ORDER BY global_seq;
上述代码中,子查询用UNION ALL把两个表堆叠起来,外层用ROW_NUMBER按创建时间和用户ID排序编号。无论两个表各自的自增ID如何重叠,global_seq都是连续且唯一的。
需要注意,OVER子句里的ORDER BY只决定编号顺序,不影响最终输出行的排列;若想让结果也按序号展示,还需在语句末尾再写一个ORDER BY。另外,ROW_NUMBER的计算发生在WHERE和GROUP BY之后,因此它可以安全用于过滤后的数据集。
二、结合PARTITION BY处理分组内序号
有时我们既想要全局序号,又想要每个分组内的序号,或者只想在某一分类下生成连续序列。PARTITION BY可以把结果集切成多个窗口,每个窗口内部重新从1开始编号。若省略PARTITION BY,则整个结果集就是一个窗口,也就得到了全局序列。
以下例子展示如何同时输出“每个城市的局部编号”和“全量数据的全局编号”:
SELECT city, user_id, ROW_NUMBER() OVER (PARTITION BY city ORDER BY create_time) AS city_seq, ROW_NUMBER() OVER (ORDER BY city, create_time) AS global_seq FROM user_all;
这里第一个窗口函数按城市分区,第二个没有任何分区,所以global_seq跨越所有城市连续递增。通过这种方式,你可以在同一行里拿到不同粒度的序号,而不用跑两遍查询。
在生成全局唯一自增序列时,通常建议ORDER BY里带上一个或多个具有唯一性的列(如主键),这样即使时间戳相同,编号结果也不会因执行计划变化而漂移,保证可重复性和稳定性。
三、与自增主键、UUID的对比
很多团队第一反应是改用UUID或雪花算法来避撞,但它们生成的是无序长字符串,在需要做有序遍历、分页或人工核对时体验较差。ROW_NUMBER方案不改表结构、不引入新组件,仅用一条SQL就能输出整齐的递增数字。
| 方案 | 是否全局唯一 | 是否递增可读 | 实现成本 |
|---|---|---|---|
| 表自增主键 | 否(单表内唯一) | 是 | 低,但跨表无效 |
| UUID | 是 | 否 | 中,需改字段类型 |
| ROW_NUMBER | 是(查询结果内) | 是 | 低,纯查询层 |
从表中可以看出,ROW_NUMBER在“查询结果内”保证唯一,这意味着它适合用于导出、报表、临时合并等场景;若业务要求写入层就必须全局唯一,则应配合分布式ID生成器,而查询展示层仍可用ROW_NUMBER做友好序号。
一个常见误区是认为ROW_NUMBER会修改底层数据,其实它只是计算列,原表一行不少、一字段不改,非常安全。
四、性能与并发注意事项
窗口函数需要在内存或临时空间中对结果集做排序,数据量很大时可能成为瓶颈。为提升性能,应在ORDER BY涉及的列上建立复合索引,让数据库尽量走索引有序扫描,减少额外排序动作。
-- 建立索引以加速ROW_NUMBER排序 CREATE INDEX idx_user_all_city_time ON user_all (city, create_time);
在并发写入频繁、且要求实时合并编号的系统中,ROW_NUMBER基于当前事务快照计算,不会锁表,但不同事务看到的数据范围不同,序号也可能不同。因此它不适合作为跨事务的“永久编号”,而更适合批处理、离线报表等最终一致性场景。
如果必须在高并发在线接口中使用,建议先把合并结果落进一张临时表或物化视图,再对临时表查ROW_NUMBER,这样既隔离了主库压力,也保证了同一份数据编号固定。
五、完整实践示例
假设我们要把订单表和历史订单表合并,导出一份带全局序号的对账单,可以按如下方式编写:
SELECT ROW_NUMBER() OVER (ORDER BY o.paid_at, o.order_no) AS seq, o.order_no, o.amount, o.paid_at FROM ( SELECT order_no, amount, paid_at FROM orders_2023 UNION ALL SELECT order_no, amount, paid_at FROM orders_history ) o ORDER BY seq;
这段代码把当年订单与历史订单堆在一起,按支付时间和订单号生成seq。由于order_no具有唯一性,相同paid_at时也不会出现编号不确定性。导出文件后,seq列就是财务可用的全局连续编号。
总结来说,利用SQL窗口函数ROW_NUMBER生成全局唯一自增序列,是一种零侵入、易理解、可维护性高的方案。只要理清分区与排序逻辑,并注意数据量与并发特征,就能在绝大多数合并查询场景中稳定落地。
SQL窗口函数ROW_NUMBER全局唯一序列修改时间:2026-08-08 10:24:37