SQL Server 存储过程分页方案
在使用 SQL Server 进行数据查询时,处理大量数据常需借助分页技术以提升性能。本文提供一段经典的通用存储过程代码,它能自动识别数据源结构(是否含主键或标识列),并动态切换最高效的分页算法,兼容从早期版本到较新版本的 SQL Server 环境。
核心原理与适应场景
该存储过程 p_show 的核心逻辑在于根据数据源的底层结构选择最优解,避免全表扫描或低效排序:
- 第一页特殊处理:若请求的是第一页,直接使用
TOP关键字返回结果,效率最高。 - 有唯一主键或标识列:优先使用此类字段进行排除法查询(
NOT IN),减少中间结果集的大小。 - 有复合主键:当主键由多个字段组成时,无法直接用于简单排除,因此采用自连接(Self-Join)的方式,先取出前 N+M 条记录的主键组合,再 LEFT JOIN 排除前 N 条,从而精准定位目标页数据。
- 无主键/非表对象:对于视图、派生表或无主键的基础表,创建一个带有自增标识列(IDENTITY)的临时表
#tb,通过范围查询(BETWEEN)截取指定页码的数据。
准备工作:存储过程安装
在执行分页前,需先在数据库中创建存储过程。以下代码包含了删除旧版本(若存在)及创建新版本的完整逻辑。请注意,该脚本使用了较早期的系统视图(如 sysobjects, sysindexes),在 SQL Server 2005 之前的版本中兼容性最好;若在高版本使用,建议测试后迁移至基于 DMV 的现代写法以获取更好性能。
IF EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[dbo].[p_show]') AND OBJECTPROPERTY(id, N'IsProcedure') = 1)
DROP PROCEDURE [dbo].[p_show]
GO
CREATE PROC p_show
@QueryStr NVARCHAR(4000), -- 表名、视图名或查询语句
@PageSize INT = 10, -- 每页大小
@PageCurrent INT = 1, -- 当前页码
@FdShow NVARCHAR(4000) = '', -- 需要显示的字段列表
@FdOrder NVARCHAR(1000) = '' -- 排序字段列表
AS
BEGIN
DECLARE
@FdName NVARCHAR(250), -- 主键或标识列名
@Id1 VARCHAR(20), @Id2 VARCHAR(20), -- 开始和结束的记录号
@Obj_ID INT, -- 对象ID
@strfd NVARCHAR(2000), -- 复合主键列表
@strjoin NVARCHAR(4000), -- 连接字段
@strwhere NVARCHAR(2000); -- 查询条件
SELECT @Obj_ID = OBJECT_ID(@QueryStr),
@FdShow = CASE ISNULL(@FdShow,'') WHEN '' THEN ' *' ELSE ' '+@FdShow END,
@FdOrder = CASE ISNULL(@FdOrder,'') WHEN '' THEN '' ELSE ' ORDER BY '+@FdOrder END,
@QueryStr = CASE WHEN @Obj_ID IS NOT NULL THEN ' '+@QueryStr ELSE ' ('+@QueryStr+') a' END;
-- 如果是第一页,直接用 TOP 完成
IF @PageCurrent = 1
BEGIN
SELECT @Id1 = CAST(@PageSize AS VARCHAR(20));
EXEC('SELECT TOP '+@Id1+@FdShow+' FROM '+@QueryStr+@FdOrder);
RETURN;
END
-- 判断是否为表,且是否有标识列
IF @Obj_ID IS NOT NULL AND OBJECTPROPERTY(@Obj_ID,'IsTable')=1
BEGIN
SELECT @Id1 = CAST(@PageSize AS VARCHAR(20)),
@Id2 = CAST((@PageCurrent-1)*@PageSize AS VARCHAR(20));
SELECT @FdName = name FROM syscolumns WHERE id=@Obj_ID AND status=0x80;
IF @@ROWCOUNT = 0 -- 无标识列,检查主键
BEGIN
IF NOT EXISTS(SELECT 1 FROM sysobjects WHERE parent_obj=@Obj_ID AND xtype='PK')
GOTO lbusetemp;
SELECT @FdName = name FROM syscolumns WHERE id=@Obj_ID AND colid IN (
SELECT colid FROM sysindexkeys WHERE @Obj_ID=id AND indid IN (
SELECT indid FROM sysindexes WHERE @Obj_ID=id AND name IN (
SELECT name FROM sysobjects WHERE xtype='PK' AND parent_obj=@Obj_ID
)
)
);
-- 检查是否为复合主键
IF @@ROWCOUNT > 1
BEGIN
SELECT @strfd='', @strjoin='', @strwhere='';
SELECT @strfd=@strfd+',['+name+']',
@strjoin=@strjoin+' AND a.['+name+']=b.['+name+']',
@strwhere=@strwhere+' AND b.['+name+'] IS NULL'
FROM syscolumns WHERE id=@Obj_ID AND colid IN (
SELECT colid FROM sysindexkeys WHERE @Obj_ID=id AND indid IN (
SELECT indid FROM sysindexes WHERE @Obj_ID=id AND name IN (
SELECT name FROM sysobjects WHERE xtype='PK' AND parent_obj=@Obj_ID
)
)
);
SELECT @strfd=SUBSTRING(@strfd,2,2000),
@strjoin=SUBSTRING(@strjoin,5,4000),
@strwhere=SUBSTRING(@strwhere,5,4000);
GOTO lbusepk;
END
END;
END
ELSE
GOTO lbusetemp;
/*--使用标识列或单一主键的处理--*/
lbuseidentity:
EXEC('SELECT TOP '+@Id1+@FdShow+' FROM '+@QueryStr+
' WHERE '+@FdName+' NOT IN(
SELECT TOP '+@Id2+' '+@FdName+' FROM '+@QueryStr+@FdOrder+
)'+@FdOrder);
RETURN;
/*--复合主键处理--*/
lbusepk:
EXEC('SELECT '+@FdShow+' FROM (
SELECT TOP '+@Id1+' a.* FROM (
SELECT TOP 100 PERCENT * FROM '+@QueryStr+@FdOrder+
') a
LEFT JOIN (
SELECT TOP '+@Id2+' '+@strfd+' FROM '+@QueryStr+@FdOrder+
) b ON '+@strjoin+'
) a WHERE '+@strwhere);
RETURN;
/*--使用临时表处理--*/
lbusetemp:
SELECT @FdName='[ID_'+CAST(NEWID() AS VARCHAR(40))+']',
@Id1 = CAST(@PageSize*(@PageCurrent-1) AS VARCHAR(20)),
@Id2 = CAST(@PageSize*@PageCurrent-1 AS VARCHAR(20));
EXEC('SELECT '+@FdName+'=IDENTITY(INT,0,1), '+@FdShow+
' INTO #tb FROM '+@QueryStr+@FdOrder+
' SELECT '+@FdShow+' FROM #tb WHERE '+@FdName+
' BETWEEN '+@Id1+' AND '+@Id2);
END
GO
第1步:调用基础分页示例
这是最简单的调用方式,仅指定表名,使用默认的每页10条数据和默认排序。
EXEC p_show '地区资料'
第2步:指定页码、字段与排序
在实际业务中,通常需要查看第5页,每页显示3条,并按特定条件排序。以下是详细参数的传入方式:
-- 语法: exec p_show '表名', 每页数量, 页码, '显示字段', '排序字段'
EXEC p_show '地区资料', 5, 3, '地区编号, 地区名称, 助记码', '地区编号'
第3步:对复杂查询或视图进行分页
如果数据来源于多表关联查询或视图,必须在查询语句前加上 TOP 100 PERCENT,以确保排序生效(部分 SQL Server 版本中子查询内的 ORDER BY 会被忽略)。
EXEC p_show 'SELECT TOP 100 PERCENT * FROM 地区资料 ORDER BY 地区名称', 5, 3, '地区编号, 地区名称', '地区名称'
关键参数说明
@QueryStr:输入的数据源。可以是简单的表名,也可以是完整的 SELECT 语句。若是后者,建议外层包上别名或使用括号处理。@PageSize:每页显示行数,默认为 10。@PageCurrent:当前要查看的页码,默认为 1。计算起始行时遵循公式:(PageCurrent- 1) *PageSize。@FdShow:需要输出的列名。若为空,默认输出所有列(*)。若原始数据包含自增 ID 等不适宜展示的字段,可在此处显式指定所需列。@FdOrder:排序依据。分页效果高度依赖稳定的排序规则,若不指定可能导致跨页数据混乱。
注意事项与排错
该脚本依赖于 SQL Server 的系统管理视图(System Views)来探测主键和标识列。如果遇到执行错误:
- 权限问题:确保当前登录账户有权限访问
sysobjects等相关系统表,通常在public角色下已具备读权限。 - 复合主键漏检:脚本中判断复合主键的逻辑较为特定,若你的数据库架构有特殊的主键定义导致误判进入临时表模式,通常也不会报错,只是性能略有下降。
- 大数据量性能:当使用临时表模式(lbusetemp)时,会将整表数据导入内存或磁盘临时表,数据量极大(千万级以上)时可能导致 IO 瓶颈。建议针对极大规模数据迁移至 SQL Server 2012+ 引入的
OFFSET...FETCH标准语法。
以上就是 SQL Server 存储过程实现分页的详细内容,更多关于 SQL Server 数据库管理与优化的资料请关注本站其它相关文章!
本站声明
本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。













