给查询结果加上一列序号,看起来是个很小的需求,但在实际项目中坑不少。比如分组后序号要不要重新计数、并列名次怎么处理、老版本的Sql Server没有窗口函数怎么办,这些细节直接决定了该选哪种实现方案。本文把Sql Server中生成序号列的几种主流做法逐一拆开讲,包括窗口函数、IDENTITY临时表和变量自增,并附上可直接运行的示例代码。

使用ROW_NUMBER生成连续序号
ROW_NUMBER是Sql Server 2005之后最推荐的方案,它属于窗口函数,作用是按照指定的排序规则为每一行分配一个从1开始的连续整数。它的基本语法是ROW_NUMBER() OVER (ORDER BY 排序字段),OVER子句里还可以加PARTITION BY实现分组编号。下面这段代码演示了最基础的用法:
-- 为员工表的结果集加上连续序号
SELECT
ROW_NUMBER() OVER (ORDER BY HireDate) AS RowNum,
EmployeeID,
EmployeeName,
HireDate
FROM Employees;
这段查询会按照入职日期从早到晚排序,并给每一行标记1、2、3这样的序号。需要注意的是,ROW_NUMBER只对查询结果生效,不会修改表结构,也不会影响原表的物理存储顺序。如果ORDER BY的字段存在重复值,序号的分配顺序在这些重复行之间是不确定的,想得到稳定结果就要在排序字段后面追加一个唯一列,比如主键。
ROW_NUMBER真正的威力在于分组编号和分页。加上PARTITION BY之后,每个分组内部会各自从1开始计数,例如按部门给员工编号:
-- 每个部门内部单独编号,从1开始
SELECT
ROW_NUMBER() OVER (PARTITION BY DepartmentID ORDER BY HireDate) AS DeptRowNum,
DepartmentID,
EmployeeName,
HireDate
FROM Employees;
这种写法在统计各部门排行、生成组内排名报表时非常常用。分页场景则通常配合CTE使用,先编号再筛选指定区间的行,比传统的TOP嵌套子查询写法更直观,性能在大多数情况下也更好。
ROW_NUMBER、RANK与DENSE_RANK的区别
这三个函数经常被放在一起比较,因为它们都挂在OVER子句下面,但处理并列值的方式完全不同。ROW_NUMBER无条件连续编号,即使两个值相同也会分配不同的序号;RANK遇到相同值会给相同的序号,但下一个不同值的序号会跳跃;DENSE_RANK同样给相同值相同的序号,但后续序号不跳跃,始终连续紧凑。
-- 三种排名函数对比
SELECT
Score,
ROW_NUMBER() OVER (ORDER BY Score DESC) AS RowNum,
RANK() OVER (ORDER BY Score DESC) AS RankNum,
DENSE_RANK() OVER (ORDER BY Score DESC) AS DenseNum
FROM StudentScores;
假设成绩数据是90、90、85、80,那么ROW_NUMBER的结果是1、2、3、4;RANK的结果是1、1、3、4;DENSE_RANK的结果是1、1、2、3。可以看到RANK出现了跳号,这是因为两个并列第一之后,第三名直接占了3的位置,符合体育比赛中的名次逻辑。DENSE_RANK则更适合需要紧凑序号的场景,比如按成绩划分档次时统计档次数。
选择哪一个取决于业务语义。排行榜、竞赛名次通常用RANK,因为并列第一之后理应是第三名;而如果只是给结果集加一个技术性的行号,比如前端列表展示序号,那ROW_NUMBER就够了;DENSE_RANK多用于去重计数和连续分组。
临时表结合IDENTITY实现序号列
如果数据库版本较老,比如还在使用Sql Server 2000,没有窗口函数可用,那么借助IDENTITY列的临时表是经典替代方案。思路是先把查询结果插入一张带有自增列的临时表,自增列自动充当序号,然后再从临时表中查询出来:
-- 使用IDENTITY临时表生成序号
SELECT IDENTITY(int, 1, 1) AS RowNum,
EmployeeID,
EmployeeName,
HireDate
INTO #TempEmployees
FROM Employees
ORDER BY HireDate;
SELECT * FROM #TempEmployees ORDER BY RowNum;
DROP TABLE #TempEmployees;
IDENTITY(int, 1, 1)表示从1开始、每次递增1的整数自增列。要注意的是,SELECT INTO配合IDENTITY的写法只能用于临时表或没有IDENTITY属性的目标表,而且插入时的ORDER BY并不被官方保证一定按此顺序生成自增值,严格来说需要依赖最后的查询排序来保证展示顺序。这种方案还会产生额外的临时表读写开销,数据量大时性能不如窗口函数。
另一个细节是并发问题。临时表以会话为隔离单位,不同连接各自创建自己的临时表副本,因此不会互相干扰,这也是为什么用#开头的临时表而不是物理表的原因。如果误用物理表做序号中转,多用户同时操作时就会出现序号混乱甚至死锁。
变量自增与排序更新的进阶用法
还有一种基于变量的自增写法,利用Sql Server中变量赋值与SELECT的执行顺序特性来累积序号。不过要提醒的是,这种写法依赖未文档化的行为,在某些版本或执行计划下结果可能不稳定,只建议在维护旧系统、无法改结构时应急使用:
-- 变量自增生成序号(不保证稳定性,慎用)
DECLARE @RowNum int = 0;
SELECT
@RowNum = @RowNum + 1 AS RowNum, -- 旧写法示意
EmployeeID,
EmployeeName
FROM Employees;
更可靠的变量方案是配合UPDATE语句更新表中的序号字段,比如已经有一列RowNum需要回填:
-- 按姓名顺序回填序号列 DECLARE @i int = 0; UPDATE Employees SET @i = RowNum = @i + 1 FROM Employees ORDER BY EmployeeName; -- 需配合TOP或索引保证顺序
这种SET @i = RowNum = @i + 1的链式赋值写法在Sql Server中可以工作,但UPDATE带ORDER BY本身不是标准语法,实际通常要借助子查询或先SELECT INTO排序后的结果再更新。总结一下选型建议:能用ROW_NUMBER就用ROW_NUMBER,需要名次语义用RANK或DENSE_RANK,老版本数据库用IDENTITY临时表过渡,变量自增方案只作为最后的兜底手段。掌握这几个函数和技巧的组合,日常开发中绝大多数序号列需求都能优雅解决。
SqlServer序号列ROW_NUMBER自动编号修改时间:2026-09-12 18:38:32