最近做一个项目,其中有许多部分需要用到分页功能
并且这些需要分页的地方有一个共同的特点,那就是,分页,但并不是按ID(主键)来排序,而是要求按其他有重复值的列进行排序,比如,其中有一个页面,要列出将近1万条客户记录,要求按客户付费金额进行排序,这种情况,如果使用网上流行的通用分页存储过程是行不能的,比如,像下面的分页存储过程虽然很棒,可是,用在这里的话,就无计可施:(这个存储过程是我在CSDN上看到一位前辈高人写的)
CREATE
PROCEDURE
GetRecordFromPage
@tblName varchar ( 255 ), -- 表名
@fldName varchar ( 255 ), -- 字段名
@PageSize int = 10 , -- 页尺寸
@PageIndex int = 1 , -- 页码
@IsCount bit = 0 , -- 返回记录总数, 非 0 值则返回
@OrderType bit = 0 , -- 设置排序类型, 非 0 值则降序
@strWhere varchar ( 4000 ) = '' -- 查询条件 (注意: 不要加 where)
AS
declare @strSQL varchar ( 6000 ) -- 主语句
declare @strTmp varchar ( 6000 ) -- 临时变量
declare @strOrder varchar ( 6000 ) -- 排序类型
if @OrderType != 0
begin
set @strTmp = " < ( select min "
set @strOrder = " order by [ " + @fldName +" ] desc "
end
else
begin
set @strTmp = " > ( select max "
set @strOrder = " order by [ " + @fldName +" ] asc "
end
set @strSQL = " select top " + str (@PageSize) + " * from [ "
+ @tblName + " ] where [ " + @fldName + " ] " + @strTmp + "( [ "
+ @fldName + " ] ) from ( select top " + str ((@PageIndex - 1 ) * @PageSize) + " [ "
+ @fldName + " ] from [ " + @tblName + " ] " + @strOrder + ") as tblTmp)"
+ @strOrder
if @strWhere != ''
set @strSQL = " select top " + str (@PageSize) + " * from [ "
+ @tblName + " ] where [ " + @fldName + " ] " + @strTmp + "( [ "
+ @fldName + " ] ) from ( select top " + str ((@PageIndex - 1 ) * @PageSize) + " [ "
+ @fldName + " ] from [ " + @tblName + " ] where (" + @strWhere + ") "
+ @strOrder + ") as tblTmp) and (" + @strWhere + ") " + @strOrder
if @PageIndex = 1
begin
set @strTmp = ""
if @strWhere != ''
set @strTmp = " where (" + @strWhere + ")"
set @strSQL = " select top " + str (@PageSize) + " * from [ "
+ @tblName + " ] " + @strTmp + " " + @strOrder
end
if @IsCount != 0
set @strSQL = " select count ( * ) as Total from [ " + @tblName + " ] "
exec (@strSQL)
GO
@tblName varchar ( 255 ), -- 表名
@fldName varchar ( 255 ), -- 字段名
@PageSize int = 10 , -- 页尺寸
@PageIndex int = 1 , -- 页码
@IsCount bit = 0 , -- 返回记录总数, 非 0 值则返回
@OrderType bit = 0 , -- 设置排序类型, 非 0 值则降序
@strWhere varchar ( 4000 ) = '' -- 查询条件 (注意: 不要加 where)
AS
declare @strSQL varchar ( 6000 ) -- 主语句
declare @strTmp varchar ( 6000 ) -- 临时变量
declare @strOrder varchar ( 6000 ) -- 排序类型
if @OrderType != 0
begin
set @strTmp = " < ( select min "
set @strOrder = " order by [ " + @fldName +" ] desc "
end
else
begin
set @strTmp = " > ( select max "
set @strOrder = " order by [ " + @fldName +" ] asc "
end
set @strSQL = " select top " + str (@PageSize) + " * from [ "
+ @tblName + " ] where [ " + @fldName + " ] " + @strTmp + "( [ "
+ @fldName + " ] ) from ( select top " + str ((@PageIndex - 1 ) * @PageSize) + " [ "
+ @fldName + " ] from [ " + @tblName + " ] " + @strOrder + ") as tblTmp)"
+ @strOrder
if @strWhere != ''
set @strSQL = " select top " + str (@PageSize) + " * from [ "
+ @tblName + " ] where [ " + @fldName + " ] " + @strTmp + "( [ "
+ @fldName + " ] ) from ( select top " + str ((@PageIndex - 1 ) * @PageSize) + " [ "
+ @fldName + " ] from [ " + @tblName + " ] where (" + @strWhere + ") "
+ @strOrder + ") as tblTmp) and (" + @strWhere + ") " + @strOrder
if @PageIndex = 1
begin
set @strTmp = ""
if @strWhere != ''
set @strTmp = " where (" + @strWhere + ")"
set @strSQL = " select top " + str (@PageSize) + " * from [ "
+ @tblName + " ] " + @strTmp + " " + @strOrder
end
if @IsCount != 0
set @strSQL = " select count ( * ) as Total from [ " + @tblName + " ] "
exec (@strSQL)
GO
后来看到CNBLOGS上另一位仁兄谈到的关于ASP.NET FORUM使用的临时表分页法,就写了一个临时表的分页存储过程,也是通用的,实现起来比较上的方法还要容易,如下
CREATE
PROCEDURE
dbo.GetPageRecord
(
@tblName varchar ( 255 ), -- 表名
@fldlist varchar ( 1000 ) = ' * ' ,
@fldName varchar ( 255 ), -- 排序字段
@KeyField varchar ( 255 ), -- 主键
@PageSize int = 10 , -- 页尺寸
@PageIndex int = 1 , -- 页码
@IsCount bit = 0 , -- 返回记录总数, 非 0 值则返回
@OrderType bit = 1 , -- 设置排序类型, 非 0 值则降序
@strWhere varchar ( 4000 ) = '' -- 查询条件 (注意: 不要加 where)
)
AS
SET NOCOUNT ON
DECLARE @PageLowerBound int
DECLARE @PageUpperBound int
-- Set the page bounds
SET @PageLowerBound = @PageSize * @PageIndex
SET @PageUpperBound = @PageLowerBound + @PageSize + 1
-- Create a temp table to store the select results
create table # temp
(
RecNo int IDENTITY ( 1 , 1 ) NOT NULL ,
oldid int
)
-- generate record
Declare @Sqlstr varchar ( 6000 )
set @sqlstr = ' select ' + @Keyfield + ' from ' + @tblname
if (@strWhere <> '' )
begin
set @Sqlstr = @sqlstr + ' where ( ' + @strWhere + ' ) '
end
set @sqlstr = @sqlstr + ' order by ' + @fldName
if (@ordertype = 0 )
begin
set @sqlstr = @sqlstr + ' asc '
end
else
begin
set @sqlstr = @sqlstr + ' desc '
end
set @sqlstr = ' insert into #temp (oldid) ' + @sqlstr
execute (@sqlstr)
set @sqlstr = ' SELECT ' + @fldList + ' FROM ' + @tblname + ' TableA (nolock), #temp T WHERE T.oldid = TableA. ' + @keyfield + ' AND T.RecNo > ' + cast (@PageLowerBound as varchar ) + ' AND T.RecNo < ' + cast (@PageUpperBound as varchar ) + ' ORDER BY T.RecNo '
execute (@sqlstr)
GO
(
@tblName varchar ( 255 ), -- 表名
@fldlist varchar ( 1000 ) = ' * ' ,
@fldName varchar ( 255 ), -- 排序字段
@KeyField varchar ( 255 ), -- 主键
@PageSize int = 10 , -- 页尺寸
@PageIndex int = 1 , -- 页码
@IsCount bit = 0 , -- 返回记录总数, 非 0 值则返回
@OrderType bit = 1 , -- 设置排序类型, 非 0 值则降序
@strWhere varchar ( 4000 ) = '' -- 查询条件 (注意: 不要加 where)
)
AS
SET NOCOUNT ON
DECLARE @PageLowerBound int
DECLARE @PageUpperBound int
-- Set the page bounds
SET @PageLowerBound = @PageSize * @PageIndex
SET @PageUpperBound = @PageLowerBound + @PageSize + 1
-- Create a temp table to store the select results
create table # temp
(
RecNo int IDENTITY ( 1 , 1 ) NOT NULL ,
oldid int
)
-- generate record
Declare @Sqlstr varchar ( 6000 )
set @sqlstr = ' select ' + @Keyfield + ' from ' + @tblname
if (@strWhere <> '' )
begin
set @Sqlstr = @sqlstr + ' where ( ' + @strWhere + ' ) '
end
set @sqlstr = @sqlstr + ' order by ' + @fldName
if (@ordertype = 0 )
begin
set @sqlstr = @sqlstr + ' asc '
end
else
begin
set @sqlstr = @sqlstr + ' desc '
end
set @sqlstr = ' insert into #temp (oldid) ' + @sqlstr
execute (@sqlstr)
set @sqlstr = ' SELECT ' + @fldList + ' FROM ' + @tblname + ' TableA (nolock), #temp T WHERE T.oldid = TableA. ' + @keyfield + ' AND T.RecNo > ' + cast (@PageLowerBound as varchar ) + ' AND T.RecNo < ' + cast (@PageUpperBound as varchar ) + ' ORDER BY T.RecNo '
execute (@sqlstr)
GO
由于性能差,导致我的WEB应用在处理分页时频频当机,于是下定决心重写新的存储过程
昨晚在家从七点到十二点一直在写,其实写分页存储过程的原理应该相当简单,下面的图反映出以下,我两种存储过程实现的不同思想
原理是很简单,大家都能想得到,问题是,过渡性结果集如何产生的问题,如果不是因为表名,条件,页码是参数化的化,我们大可以用一句SELECT TOP来完成它,可是,问题是,不能,因为它们是参数化的,下面的关键性内容将解除你这方面的顾虑,让我们一起来研究一下MS SQL SERVER中隐藏的秘密吧:
第一个秘密是关于SELECT语句的:
SQL中的SELECT语句可以将另外一个SELECT语句作为内容来源,并且按此规律进行嵌套处理,但是,有一个前提,必须对作为来源的SELEC结果集指定别名,下面是一个示例性SQL语句,你可以在查询分析器里证实它:
select
top
10
*
from
(
select
top
100
*
from
tblproducts
where
name
like
'
%mp3%
'
order
by
id
desc
)
as
a
order
by
id
desc
第二个秘密是一个规则:
如果你想用select top 20*4的方式在SQL中取前80条记录,你会失败,因为TOP子句有一个限制,就是TOP num中的num不能是一个计算值或表达式而必须是一个既定的常量
第三个秘密足够重大,我想,应该很少有人发现它,否则的话,很多问题便会变得简单起来,简要的说,它就是:SQL允许你用EXECUTE执行一个变量中定义的SQL语句,并且允许你在被执行的SQL语句中,再次嵌套入一个变量定义的语句,并且再次在其中用EXECUTE执行它,如果语言不足以证明,那么下面的代码将证明这是行得通并且是很好的
declare
@sqlstr
varchar
(
3000
)
set @sqlstr = ' declare @subsqlstr varchar(1000); '
set @sqlstr = @sqlstr + ' set @subsqlstr= '' select * from tblproducts '' ; '
set @sqlstr = @sqlstr + ' execute (@subsqlstr) '
execute (@sqlstr)
set @sqlstr = ' declare @subsqlstr varchar(1000); '
set @sqlstr = @sqlstr + ' set @subsqlstr= '' select * from tblproducts '' ; '
set @sqlstr = @sqlstr + ' execute (@subsqlstr) '
execute (@sqlstr)
在上面的代码中,@SQL变量中定义的是一组SQL语句,在这组SQL语句中,又定义了一个@SUBSQL变量,在其中存放了SELECT语句,
所以,当系统执行@SQLSTR时,其中定义的变量会被执行,执行的结果是,构造了一组动态执行的SQL语句,将其存入了变量,最后,在EXECUTE中又被执行,这种嵌套的执行,我们就姑且称其为动态执行能力吧
不过,需要相当注意的是:
1,由于在变量中存放语句组,因此,且莫忘记在每一个语句末尾加上分号(分号是SQL一句语句的结束标志)
2,如果在语句中包含变量的赋值,请记住你是在写一个变量赋值语句,因此记住加上单引号将值括起
3,如果你在变量中定义的语句组中要引用字符串常量,请先将单引号替换成两个单引号
最后,我用上面的方法写了两种SQL通用分页存储过程:
它们具有以下特点1,支持字段集合选择,2支持任意字段排序,
上面的图,我们以最终结果集倒序为例,第一种方案,我们先取集集合OB,这可以用一个使用SELECT TOP 并使用升级排列的语句完成
然后对OB结果集进行倒序排序,再用"SELECT TOP 每页记录数"倒序 的方式取得目标集合AB,下面是存储过程
create
PROCEDURE
dbo.GetPagingData
(
@tablename varchar ( 100 ), -- 表名或视图表
@fieldlist varchar ( 4000 ) = ' * ' , -- 欲选择字段列表
@orderfield varchar ( 100 ), -- 排序字段
@keyfield varchar ( 100 ), -- 主键
@pageindex int , -- 页号,从0开始
@pagesize int = 20 , -- 页尺寸
@strwhere varchar ( 4000 ), -- 条件
@ordertype bit = 1 -- 排序,1,降序,0,升序
)
AS
/*
名称:GetPagingRecord
作用:按任意字段进行排序分页
作者:菩提树(MARK MA)
时间:2004-12-14
声明:此代码你可以无偿使用及转载,但在转载时,请勿移称本文字声明
*/
SET NOCOUNT ON
declare @sqlstr varchar ( 6000 )
-- 处理SQL中危险字符,并且将条件处理成易嵌入的形式
set @sqlstr = ' declare @Rcount int; '
set @sqlstr = @sqlstr + ' set @rcount=(select count( ' + @keyfield + ' ) from ' + @tablename + ' where ' + @strWhere + ' ); '
set @strwhere = replace (@strwhere, '''' , '''''' )
set @strwhere = replace (@strwhere, ' -- ' , '' )
set @strwhere = replace (@strwhere, ' ; ' , '' )
set @sqlstr = @sqlstr + ' declare @Rnum int; '
set @sqlstr = @sqlstr + ' set @rnum=@rcount- ' + cast (@pagesize as varchar ) + ' * ' + cast (@pageindex as varchar ) + ' ; '
set @sqlstr = @sqlstr + ' declare @sqlstr varchar(6000); '
if @ordertype = 1
begin
set @sqlstr = @sqlstr + ' set @sqlstr= '' select top ' + cast (@Pagesize as varchar ) + ' ' + @fieldlist + ' from (select top 100
percent * from (select top '' +cast(@rnum as varchar)+ '' * from ' + @tablename + ' where ' + @strwhere + '
order by ' + @orderfield + ' asc) as b order by paymoney desc) as a order by ' + @orderfield + ' desc '' ; '
end
else
begin
set @sqlstr = @sqlstr + ' set @sqlstr= '' select top ' + cast (@Pagesize as varchar ) + ' ' + @fieldlist + ' from (select top 100
percent * from (select top '' +cast(@rnum as varchar)+ '' * from ' + @tablename + ' where ' + @strwhere + '
order by ' + @orderfield + ' desc) as b order by paymoney asc) as a order by ' + @orderfield + ' asc '' ; '
end
set @sqlstr = @sqlstr + ' if @Rcount>0 begin execute(@sqlstr) end '
-- print @sqlstr
execute (@sqlstr)
(
@tablename varchar ( 100 ), -- 表名或视图表
@fieldlist varchar ( 4000 ) = ' * ' , -- 欲选择字段列表
@orderfield varchar ( 100 ), -- 排序字段
@keyfield varchar ( 100 ), -- 主键
@pageindex int , -- 页号,从0开始
@pagesize int = 20 , -- 页尺寸
@strwhere varchar ( 4000 ), -- 条件
@ordertype bit = 1 -- 排序,1,降序,0,升序
)
AS
/*
名称:GetPagingRecord
作用:按任意字段进行排序分页
作者:菩提树(MARK MA)
时间:2004-12-14
声明:此代码你可以无偿使用及转载,但在转载时,请勿移称本文字声明
*/
SET NOCOUNT ON
declare @sqlstr varchar ( 6000 )
-- 处理SQL中危险字符,并且将条件处理成易嵌入的形式
set @sqlstr = ' declare @Rcount int; '
set @sqlstr = @sqlstr + ' set @rcount=(select count( ' + @keyfield + ' ) from ' + @tablename + ' where ' + @strWhere + ' ); '
set @strwhere = replace (@strwhere, '''' , '''''' )
set @strwhere = replace (@strwhere, ' -- ' , '' )
set @strwhere = replace (@strwhere, ' ; ' , '' )
set @sqlstr = @sqlstr + ' declare @Rnum int; '
set @sqlstr = @sqlstr + ' set @rnum=@rcount- ' + cast (@pagesize as varchar ) + ' * ' + cast (@pageindex as varchar ) + ' ; '
set @sqlstr = @sqlstr + ' declare @sqlstr varchar(6000); '
if @ordertype = 1
begin
set @sqlstr = @sqlstr + ' set @sqlstr= '' select top ' + cast (@Pagesize as varchar ) + ' ' + @fieldlist + ' from (select top 100
percent * from (select top '' +cast(@rnum as varchar)+ '' * from ' + @tablename + ' where ' + @strwhere + '
order by ' + @orderfield + ' asc) as b order by paymoney desc) as a order by ' + @orderfield + ' desc '' ; '
end
else
begin
set @sqlstr = @sqlstr + ' set @sqlstr= '' select top ' + cast (@Pagesize as varchar ) + ' ' + @fieldlist + ' from (select top 100
percent * from (select top '' +cast(@rnum as varchar)+ '' * from ' + @tablename + ' where ' + @strwhere + '
order by ' + @orderfield + ' desc) as b order by paymoney asc) as a order by ' + @orderfield + ' asc '' ; '
end
set @sqlstr = @sqlstr + ' if @Rcount>0 begin execute(@sqlstr) end '
-- print @sqlstr
execute (@sqlstr)
在上面的代码中,还处理了没有符合条件结果的情况
第二种方案的思想是这样的,先用倒序的SELECT TOP (页序号+1)*页尺寸的方法取得AE结果集,再从AE结果集中用NOT IN 的方法排除掉用SELECT TOP 页序号*页尺寸的方法取得的结果集,最后,对目标结果集执行倒序,下面是实现的代码
create
PROCEDURE
dbo.GetPagingRecord
(
@tablename varchar ( 100 ), -- 表名或视图表
@fieldlist varchar ( 4000 ) = ' * ' , -- 欲选择字段列表
@orderfield varchar ( 100 ), -- 排序字段
@keyfield varchar ( 100 ), -- 主键
@pageindex int , -- 页号,从0开始
@pagesize int = 20 , -- 页尺寸
@strwhere varchar ( 4000 ), -- 条件
@ordertype bit = 1 -- 排序,1,降序,0,升序
)
AS
/*
名称:GetPagingRecord
作用:按任意字段进行排序分页
作者:菩提树(MARK MA)
时间:2004-12-14
声明:此代码你可以无偿使用及转载,但在转载时,请勿移称本文字声明
*/
SET NOCOUNT ON
declare @sqlstr varchar ( 6000 )
-- 处理SQL中危险字符,并且将条件处理成易嵌入的形式
set @strwhere = replace (@strwhere, '''' , '''''' )
set @strwhere = replace (@strwhere, ' -- ' , '' )
set @strwhere = replace (@strwhere, ' ; ' , '' )
set @sqlstr = ' declare @CurPageNum int; '
set @sqlstr = @sqlstr + ' declare @nextpagenum int; '
set @sqlstr = @sqlstr + ' set @curpagenum= ' + cast (@PageIndex as varchar ) + ' * ' + cast (@Pagesize as varchar ) + ' ; '
set @sqlstr = @sqlstr + ' set @nextpagenum= ' + cast (@PageIndex + 1 as varchar ) + ' * ' + cast (@Pagesize as varchar ) + ' ; '
set @sqlstr = @sqlstr + ' declare @sqlstr varchar(6000); '
if @ordertype = 1
begin
set @sqlstr = @sqlstr + ' set @sqlstr= '' select ' + @fieldlist + ' from ( select top '' +cast(@nextpagenum as varchar)+ '' * from
' + @tablename + ' where ' + @strwhere + ' order by ' + @orderfield + ' desc ) as a where ' + @keyfield + ' not in (
select top '' +cast(@curpagenum as varchar)+ '' ' + @keyfield + ' from ' + @tablename + ' where ' + @strwhere + '
order by ' + @orderfield + ' desc) order by ' + @orderfield + ' desc '' ; '
end
else
begin
set @sqlstr = @sqlstr + ' set @sqlstr= '' select ' + @fieldlist + ' from ( select top '' +cast(@nextpagenum as varchar)+ '' * from
' + @tablename + ' where ' + @strwhere + ' order by ' + @orderfield + ' asc ) as a where ' + @keyfield + ' not in (
select top '' +cast(@curpagenum as varchar)+ '' ' + @keyfield + ' from ' + @tablename + ' where ' + @strwhere + '
order by ' + @orderfield + ' asc) order by ' + @orderfield + ' asc '' ; '
end
set @sqlstr = @sqlstr + ' execute( @sqlstr) '
-- print @sqlstr
execute (@sqlstr)
(
@tablename varchar ( 100 ), -- 表名或视图表
@fieldlist varchar ( 4000 ) = ' * ' , -- 欲选择字段列表
@orderfield varchar ( 100 ), -- 排序字段
@keyfield varchar ( 100 ), -- 主键
@pageindex int , -- 页号,从0开始
@pagesize int = 20 , -- 页尺寸
@strwhere varchar ( 4000 ), -- 条件
@ordertype bit = 1 -- 排序,1,降序,0,升序
)
AS
/*
名称:GetPagingRecord
作用:按任意字段进行排序分页
作者:菩提树(MARK MA)
时间:2004-12-14
声明:此代码你可以无偿使用及转载,但在转载时,请勿移称本文字声明
*/
SET NOCOUNT ON
declare @sqlstr varchar ( 6000 )
-- 处理SQL中危险字符,并且将条件处理成易嵌入的形式
set @strwhere = replace (@strwhere, '''' , '''''' )
set @strwhere = replace (@strwhere, ' -- ' , '' )
set @strwhere = replace (@strwhere, ' ; ' , '' )
set @sqlstr = ' declare @CurPageNum int; '
set @sqlstr = @sqlstr + ' declare @nextpagenum int; '
set @sqlstr = @sqlstr + ' set @curpagenum= ' + cast (@PageIndex as varchar ) + ' * ' + cast (@Pagesize as varchar ) + ' ; '
set @sqlstr = @sqlstr + ' set @nextpagenum= ' + cast (@PageIndex + 1 as varchar ) + ' * ' + cast (@Pagesize as varchar ) + ' ; '
set @sqlstr = @sqlstr + ' declare @sqlstr varchar(6000); '
if @ordertype = 1
begin
set @sqlstr = @sqlstr + ' set @sqlstr= '' select ' + @fieldlist + ' from ( select top '' +cast(@nextpagenum as varchar)+ '' * from
' + @tablename + ' where ' + @strwhere + ' order by ' + @orderfield + ' desc ) as a where ' + @keyfield + ' not in (
select top '' +cast(@curpagenum as varchar)+ '' ' + @keyfield + ' from ' + @tablename + ' where ' + @strwhere + '
order by ' + @orderfield + ' desc) order by ' + @orderfield + ' desc '' ; '
end
else
begin
set @sqlstr = @sqlstr + ' set @sqlstr= '' select ' + @fieldlist + ' from ( select top '' +cast(@nextpagenum as varchar)+ '' * from
' + @tablename + ' where ' + @strwhere + ' order by ' + @orderfield + ' asc ) as a where ' + @keyfield + ' not in (
select top '' +cast(@curpagenum as varchar)+ '' ' + @keyfield + ' from ' + @tablename + ' where ' + @strwhere + '
order by ' + @orderfield + ' asc) order by ' + @orderfield + ' asc '' ; '
end
set @sqlstr = @sqlstr + ' execute( @sqlstr) '
-- print @sqlstr
execute (@sqlstr)
需要注意的是,如果要避免SQL注入式攻击,请注意处理像分号,双减号,单引号这些在SQL中有特殊含义的字符
至于上面两个存储过程哪个性能更好,那就取决于是对一个倒序的结果集再进入一次反序排列好呢,还是用NOT IN从一个大的结果集移除一个小的结果集好
希望对大家有帮助
如果需要转载,请标明出处,勿删减内容