剛搬家到CSDN,決定貢獻第一篇文章。:)
最近做一個新項目,想使用一下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
- 測試表插入300萬條記錄,按照如下分頁規則測試:
1) 每頁顯示30條記錄,返回第10頁
2) 每頁顯示30條記錄,返回第1000頁
3) 每頁顯示30條記錄,返回第10000頁
4) 每頁顯示20條記錄,返回第100000頁
測試腳本如下:

--分頁性能測試

--1 建立測試表

CREATE TABLE [dbo].[tb_group](

[ID] [int] IDENTITY(1,1) NOT NULL,

[Name] [nvarchar](50) NULL,

[city] [nvarchar](50) NULL,

[province] [nvarchar](50) NULL,

[country] [nvarchar](50) NULL,

CONSTRAINT [PK_tb_group] PRIMARY KEY CLUSTERED

(

[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 @count int

declare @ch char(4)

declare @c nvarchar(2)

declare @i int

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 分頁

create proc sp_pagination1

@pageindex int=1,

@pagesize int=10,

@ordercolumn nvarchar(20)='''',

@order bit =0,-- 0 is''ASC'' 1 is ''DESC''

@totalcount int output

as

declare @sql nvarchar(4000)

declare @strOrder1 nvarchar (4)

declare @strOrder2 nvarchar (4)

declare @strOrderColumn nvarchar(60)

select @totalcount=count(id) from tb_group

if (@order=0)

begin

set @strOrder1 =''asc''

set @strOrder2 =''desc''

end else begin

set @strOrder1 =''desc''

set @strOrder2 =''asc''

end

if (@ordercolumn<>'''')

begin

set @strOrderColumn=''order by '' + @ordercolumn + '' ''

end else begin

set @strOrderColumn=''''

set @strOrder1 =''''

set @strOrder2 =''''

end

if (@pageindex<=0 or @pagesize<=0 or (@pagesize*(@pageindex-1)>@totalcount)) begin

set @sql=''select * from tb_group where 1<>1''

end else begin

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''

end else begin

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)

--方法二

-- 臨時表分頁

create proc sp_pagination2

@pageindex int=1,

@pagesize int=10,

@ordercolumn nvarchar(20)='''',

@order bit =0,-- 0 is''ASC'' 1 is ''DESC''

@totalcount int output

as

declare @sql nvarchar(4000)

declare @strOrder nvarchar (4)

declare @strOrderColumn nvarchar(60)

if (@order=0)

begin

set @strOrder =''asc''

end else begin

set @strOrder =''desc''

end

if (@ordercolumn<>'''')

begin

set @strOrderColumn=''order by '' + @ordercolumn + '' ''

end else begin

set @strOrderColumn=''''

set @strOrder =''''

end

select @totalcount=count(id) from tb_group

create table #pager (id int, pagerid int identity(1, 1) not null)

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?

select * from (select *,row_number() over (order by country) as rowno from tb_group) as t1

where rowno > 0 and rowno < 61

SELECT * FROM (SELECT *,ROW_NUMBER() OVER (ORDER BY country) AS RowNo FROM tb_group) AS T1

WHERE RowNo > 30 and RowNo < 61

--下面是測試代碼

create proc sp_pagination3

@pageindex int=1,

@pagesize int=10,

@ordercolumn nvarchar(20)='''',

@order bit =0,-- 0 is''ASC'' 1 is ''DESC''

@totalcount int output

as

declare @sql nvarchar(4000)

declare @strOrder nvarchar (4)

declare @strOrderColumn nvarchar(60)

if (@order=0)

begin

set @strOrder =''asc''

end else begin

set @strOrder =''desc''

end

if (@ordercolumn<>'''')

begin

set @strOrderColumn=''order by '' + @ordercolumn + '' ''

end else begin

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)
測試用例代碼:

--測試在排序 “無索引列” 時的分頁性能

DBCC FREEPROCCACHE

GO

select * from tb_group order by country asc

--DBCC FREESESSIONCACHE

declare @starttime datetime

declare @endtime datetime

declare @time datetime

declare @total int

set @starttime=getdate()

exec sp_pagination3 1,10,''country'',0,@total output

set @endtime=getdate()

set @time=@endtime-@starttime

select @total,@time

--測試在排序 “有索引列” 時的分頁性能

DBCC FREEPROCCACHE

GO

--DBCC FREESESSIONCACHE

select * from tb_group

declare @starttime datetime

declare @endtime datetime

declare @time datetime

declare @total int

set @starttime=getdate()

exec sp_pagination3 3,5,''id'',0,@total output

set @endtime=getdate()

set @time=@endtime-@starttime

select @total,@time
測試結果:
利用TOP N 分頁
測試項目
測試用時
無索引列排序
有索引列排序
每頁顯示30條記錄,返回第10頁 :
1:05分220毫秒
每頁顯示30條記錄,返回第1000頁 :
1:07分
280毫秒
每頁顯示30條記錄,返回第10000頁 :
1:07分
640毫秒
每頁顯示20條記錄,返回第100000頁 :
1:15分
2秒640毫秒
臨時表分頁
測試項目
測試用時
無索引列排序
有索引列排序
每頁顯示30條記錄,返回第10頁 :
27秒407毫秒
2秒627毫秒
每頁顯示30條記錄,返回第1000頁 :
28秒170
毫秒
2秒873毫秒
每頁顯示30條記錄,返回第10000頁 :
31秒500毫秒
5秒783毫秒
每頁顯示20條記錄,返回第100000頁 :
45秒937毫秒
15秒750毫秒
ROW_NUMBER 分頁
測試項目
測試用時
無索引列排序
有索引列排序
每頁顯示30條記錄,返回第10頁 :
9秒107毫秒
250毫秒
每頁顯示30條記錄,返回第1000頁 :
9秒873毫秒
263毫秒
每頁顯示30條記錄,返回第10000頁 :
12秒230毫秒
517毫秒
每頁顯示20條記錄,返回第100000頁 :
19秒640毫秒
1秒983毫秒
結果分析
- 利用TOP N 分頁
缺點: 如果排序列不是索引列,性能低下。最後一頁的顯示會錯誤。除非添加代碼對最後一頁進行處理。另外如果排序列有重復值,排序發生錯誤,解決方案是進行二次排序。 (e.g.: order by column1, PKcolum)。需要編寫代碼判斷pageindex的有效性。如果用時間列作為排序列似乎需要進一步更改代碼,筆者未作進一步嘗試。
- 利用臨時表分頁
缺點: 消耗服務器IO,如果數據量大,可能因磁盤性能導致查詢速度下降。 如果插入臨時表時不使用top 性能下降,如果使用top,對有重復值的列排序會不正確。解決方案是進行二次排序.
- ROW_NUMBER 分頁
缺點: 結果較為滿意,缺點是只能用於SQL2005
從上面的分析可以看出,在有條件的情況下(有SQL2005)應該優先考慮使用ROW_NUMBER 排序。此外,如果僅對於索引列排序,可以使用TOP N排序獲得較好的性能。但是如果需要使用無索引列排序,可以考慮使用臨時表。或者修改表結構建立索引。