本文作者Eric Z.通过对比测试,探讨了SQL Server 2005中利用TOP N、临时表和ROW_NUMBER()三种分页方法的性能。测试环境为Windows Server 2003和SQL Server 2005。测试发现,ROW_NUMBER()在SQL2005中表现优秀,但只适用于此版本;TOP N在索引列排序时效率较高,但存在一些限制;而临时表方法可能导致IO消耗。结论建议根据实际情况选择合适分页策略。
最近做一个新项目,想使用一下SQL Server 2005中的 ROW_NUMBER的分页方式,因为这个新的特性可以使得分页代码的编写变得轻松。唯一担心的是性能问题,于是我上网搜索了一下关于 ROW_NUMBER的性能的讨论,可是发现有些人的测试表明,该特性形同鸡肋,并不能带来性能的显著提高。对此我颇为怀疑,为了求证这种分页方法的的性能到底如何。只好亲自操刀,对于现在流行的3种分页方法做一个对比测试。
对比测试的方法如下:
利用TOP N 分页
利用临时表分页
ROW_NUMBER 分页
测试环境:
Windows Server 2003
Intel Core2 6300/4G 内存
Microsoft SQL Server 2005 - 9.00.3054.00 (Intel X86) Mar 23 2007 16:28:52 Copyright (c) 1988-2005 Microsoft Corporation Enterprise Edition on Windows NT 5.2 (Build 3790: Service Pack 2)
测试说明:
分别用三种方法建立Stored procedures参数统一如下: @pageindex int=1, @pagesize int=10, @ordercolumn nvarchar(20)='', @order bit =0,-- 0 stands for 'ASC' 1 stands for 'DESC' @totalcount int output
--分页性能测试 --1 建立测试表 CREATETABLE[dbo].[tb_group]( [ID][int]IDENTITY(1,1) NOTNULL, [Name][nvarchar](50) NULL, [city][nvarchar](50) NULL, [province][nvarchar](50) NULL, [country][nvarchar](50) NULL, CONSTRAINT[PK_tb_group]PRIMARYKEYCLUSTERED ( [ID]ASC )WITH (PAD_INDEX =OFF, STATISTICS_NORECOMPUTE =OFF, IGNORE_DUP_KEY =OFF, ALLOW_ROW_LOCKS =ON, ALLOW_PAGE_LOCKS =ON) ON[PRIMARY] ) ON[PRIMARY] --2 插入300万条记录 select@@version declare@countint declare@chchar(4) declare@cnvarchar(2) declare@iint set@count=3000000 while@count>0 begin set@i=65+floor(rand()*26) set@c=convert(nvarchar(2),@i) set@ch=replicate(char(@i),4) insert tb_group ([name],[city],[province],[country]) values (@ch,'C'+@c,'P'+@c,@ch) set@count=@count-1 end --select count(*) from tb_group --方法一 -- TOP N 分页 createproc sp_pagination1 @pageindexint=1, @pagesizeint=10, @ordercolumnnvarchar(20)='', @orderbit=0,-- 0 is'ASC' 1 is 'DESC' @totalcountint output as declare@sqlnvarchar(4000) declare@strOrder1nvarchar (4) declare@strOrder2nvarchar (4) declare@strOrderColumnnvarchar(60) select@totalcount=count(id) from tb_group if (@order=0) begin set@strOrder1='asc' set@strOrder2='desc' endelsebegin set@strOrder1='desc' set@strOrder2='asc' end if (@ordercolumn<>'') begin set@strOrderColumn='order by '+@ordercolumn+'' endelsebegin set@strOrderColumn='' set@strOrder1='' set@strOrder2='' end if (@pageindex<=0or@pagesize<=0or (@pagesize*(@pageindex-1)>@totalcount)) begin set@sql='select * from tb_group where 1<>1' endelsebegin if (@pagesize*@pageindex>@totalcount) begin set@sql='select * from '+ '(select top '+convert(nvarchar(10),@totalcount-(@pagesize*(@pageindex-1))) + ' * from tb_group '+@strOrderColumn+@strOrder2+',id desc) t1 '+ @strOrderColumn+@strOrder1+',id' endelsebegin set@sql='select * from '+ '(select top '+convert(nvarchar(10),@pagesize)+' * from '+ '(select top '+convert(nvarchar(10),@pagesize+@pagesize*(@pageindex-1)) + ' * from tb_group '+@strOrderColumn+@strOrder1+',id) t1 '+@strOrderColumn+@strOrder2+ ',id desc) t2 '+@strOrderColumn+@strOrder1+',id' end end exec (@sql) --方法二 -- 临时表分页 createproc sp_pagination2 @pageindexint=1, @pagesizeint=10, @ordercolumnnvarchar(20)='', @orderbit=0,-- 0 is'ASC' 1 is 'DESC' @totalcountint output as declare@sqlnvarchar(4000) declare@strOrdernvarchar (4) declare@strOrderColumnnvarchar(60) if (@order=0) begin set@strOrder='asc' endelsebegin set@strOrder='desc' end if (@ordercolumn<>'') begin set@strOrderColumn='order by '+@ordercolumn+'' endelsebegin set@strOrderColumn='' set@strOrder='' end select@totalcount=count(id) from tb_group createtable #pager (id int, pagerid intidentity(1, 1) notnull) set@sql='insert into #pager (id) '+ 'select top '+convert(varchar(10),@pageindex*@pagesize) +' id from tb_group '+@strOrderColumn+@strOrder+',id' exec (@sql) set@sql='select * from tb_group as A inner join #pager as B on A.id=B.id'+ ' where b.pagerid > '+convert(varchar(10),@pagesize* (@pageindex-1))+' and b.pagerid < '+ convert(varchar(10),@pagesize* (@pageindex) +1) exec (@sql) --方法三 -- ROW_Number分页 --在开始测试这个方法之前我发现一个问题,下面两个语句照道理应该一模一样,可是上面用小写英文的却无法执行,查询KB也没有发现相关问题。莫非我发现了一个BUG?(重新连接DB后此问题消失) select*from (select*,row_number() over (orderby country) as rowno from tb_group) as t1 where rowno >0and rowno <61 SELECT*FROM (SELECT*,ROW_NUMBER() OVER (ORDERBY country) AS RowNo FROM tb_group) AS T1 WHERE RowNo >30and RowNo <61 --下面是测试代码 createproc sp_pagination3 @pageindexint=1, @pagesizeint=10, @ordercolumnnvarchar(20)='', @orderbit=0,-- 0 is'ASC' 1 is 'DESC' @totalcountint output as declare@sqlnvarchar(4000) declare@strOrdernvarchar (4) declare@strOrderColumnnvarchar(60) if (@order=0) begin set@strOrder='asc' endelsebegin set@strOrder='desc' end if (@ordercolumn<>'') begin set@strOrderColumn='order by '+@ordercolumn+'' endelsebegin set@strOrderColumn='' set@strOrder='' end select@totalcount=count(id) from tb_group set@sql='SELECT * FROM (SELECT *,ROW_NUMBER() OVER ('+@strOrderColumn+@strOrder+') AS RowNo FROM tb_group) AS T1'+ ' WHERE RowNo > '+convert(varchar(10),@pagesize* (@pageindex-1)) + ' and RowNo < '+convert(varchar(10),@pagesize* (@pageindex) +1) exec (@sql)
缺点: 如果排序列不是索引列,性能低下。最后一页的显示会错误。除非添加代码对最后一页进行处理。另外如果排序列有重复值,排序发生错误,解决方案是进行二次排序。 (e.g.: order by column1, PKcolum)。需要编写代码判断pageindex的有效性。如果用时间列作为排序列似乎需要进一步更改代码,笔者未作进一步尝试。