程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 數據庫知識 >> SqlServer數據庫 >> 關於SqlServer >> 談起存儲過程分頁

談起存儲過程分頁

編輯:關於SqlServer

從存儲過程分頁談起

  為什麼要選擇用存儲過程分頁呢?其實原因很簡單,數據庫查詢功能的性能終究是有限的。即使我們對數據庫進行了最優配置,對數據表設計再三斟酌,然而一旦面臨海量數據,且返回結果集較大的時候,常規的查詢語句就無能為力了。一般說來,當返回的結果集超過總數量的40%時,數據庫層面上的優化就顯得束手無策了。此時,我相信大多數同行首先想到的便是分頁。當我們指定好每頁的記錄總數(PageSize)和當前頁的索引(CurrentPage)時,理想的狀況便發生了,首先我們不再從一個海量數據(百萬級)中檢索出超過40%的數據量,我們可以做個估算如果每頁顯示50條記錄,那麼也就是從100萬條記錄中查詢50條記錄,這個比例我相信大家都比較清楚。其次,網絡中的數據通信量將大大縮減,我想這筆帳就不用我再做過多解釋。同時,查詢數量的減少對內存開銷、頁面的刷新、用戶的等待時間都會得到相應的減少。

  好處頗多,如何實現呢?我大致總結了以下幾種實現方式。下面,我將一一介紹:

  我將表分成兩類

  1.數據表中有唯一的自增索引,並且這個字段沒有出現斷號現象。在此我姑且稱之為連續表,後面文章中出現的連續表就是指此類表。

  2.數據表中不存在唯一的自增索引,或者存在唯一自增索引,但是由於刪除記錄等操作讓該索引不連續,對於這類表我稱之為不聯系表。

  分頁之前我們模擬一張產品表,其結構如下圖:

  插入1000000條記錄

DECLARE @I INT

SET @I=0

WHILE(@I<1000000)

BEGIN

INSERT INTO PRODUCT(ProductName,ProductAddDate) VALUES('產品名',GETDATE())

SET  @I=@I+1

  同時給出存儲過程的結構,由於今天只討論分頁,所以查詢條件、排序分組方法等請讀者自行補充。

CREATE PROCEDURE SelectProduct

 @PageSize int,

 @CurrentPage int,

 @TotalPage int output

BEGIN

 --select method

  說明:由於上一篇文章的分頁控件需要一個總頁數的參數,因此將@TotalPage 作為輸出參數返回符合記錄的總頁數。@CurrentPage為0表示

  第一頁。

  執行存儲過程代碼

SET   STATISTICS   PROFILE   ON   

SET   STATISTICS   IO   ON   

SET   STATISTICS   TIME   ON   

DECLARE @return_value int,

  @TotalPage int

EXEC @return_value = [dbo].[SelectProduct]

  @PageSize = 50,

  @CurrentPage = 1,

  @TotalPage = @TotalPage OUTPUT

SELECT @TotalPage as N'@TotalPage'

SELECT 'Return Value' = @return_value

SET   STATISTICS   PROFILE   OFF   

SET   STATISTICS   IO   OFF   

SET   STATISTICS   TIME   OFF  

  測試環境:

  Win2003、SqlServer2005、720775條記錄、每頁50條記錄、本機直接訪問數據庫、每組10次查詢取平均值

  連續表的分頁方案:

  方案:利用ID篩選出要得到的數據

CREATE PROCEDURE SelectProduct

 @PageSize int,

 @CurrentPage int,

 @TotalPage int output

BEGIN

 EXEC('SELECT TOP  '+@PageSize+'* FROM Product WHERE ProductId>('+@PageSize+'*'+@CurrentPage+')')

 SELECT @TotalPage=COUNT(*)/@PageSize FROM Product

  說明:SELECT TOP 後面不能直接跟變量,所以采用了拼接sql的辦法

  測試結果:

頁碼            執行時間(ms)  

1                  1

100              2

1000            4

5000            9

10000          13

14000          16

  分析結果發現在百萬級數據都在小於0.1秒,這足以滿足大多數要求,但是隨著數據頁的增大呈現一種查詢變緩的趨勢。

  當然還有其他對ID進行比較的如:between and  and so,當然還有游標分頁,雖然通用性很好,但是性能很差。今天我就不一一列舉,因

  為今天要討論的重點是不連續表的分頁技術。

  不連續表的分頁技術

  為了讓上面的表不連續我們將部分記錄刪除

DELETE FROM Product WHERE ProductId%24=0

  執行完成後(30032 行受影響)即30032條記錄被刪除,如果我們再用連續表的分頁方式在此表上分頁就不再適用。因為檢索的記錄中存在斷

  號現象。所以我們需尋求新的方法分頁

  方案一:重建數據表的唯一自增索引

DBCC CHECKIDENT (Product, RESEED,1) 

  重建之後采用連續表的分頁方式,因為該方式是效率最高的分頁查詢。該方案不適用於將該唯一自增索引作為其他表外鍵的關系型數據庫,

  這樣將會導致數據混亂,望慎用。

  方案二:采用臨時表分頁

  局部臨時表的生存期一次會話過程,說得簡單點就是當一個用戶執行一個查詢時創建,查詢執行完成後自動刪除。

CREATE  PROCEDURE [dbo].[SelectProduct]

 @PageSize int,

 @CurrentPage int,

 @TotalPage int output

BEGIN

 DECLARE @BeginID INT ,@EndID INT 

 SET @BeginId=@PageSize*@CurrentPage

 SET @EndID=@PageSize*(@CurrentPage+1)

 CREATE TABLE #TmpProduct(

 Id int IDENTITY(1,1) PRIMARY KEY,

 ProductId int not null)

 INSERT INTO #TmpProduct(ProductId) SELECT ProductId FROM Product

 SET @TotalPage=@@ROWCOUNT/@PageSize

 SELECT * FROM Product as p,#TmpProduct as t WHERE p.ProductId=t.ProductId and t.ProductId BETWEEN @BeginId AND 

@EndID

  測試結果:

記錄總數      查詢時間(ms)

10000         198

100000       669

250000       1454

500000       3980

700000       5543

  由此我們發現規律,當數據量越小查詢速度也就越快,因此該方法適用於小數據量的表。從執行計劃中發現向臨時表中插入數據占用了整個

  查詢過程的90%-95%時間,而真正查詢我們想要的產品記錄僅僅占了5%-10%,那麼有沒有辦法不去反復執行插入過程呢?那麼我們可以采用

  全局臨時表或者普通表,代碼如下:

  首先創建全局臨時表並插入記錄:

CREATE TABLE ##TmpProduct(

 Id int IDENTITY(1,1) PRIMARY KEY,

 ProductId int not null)

 INSERT INTO ##TmpProduct(ProductId) SELECT ProductId FROM Product

  重寫存儲過程

CREATE  PROCEDURE [dbo].[SelectProduct]

 @PageSize int,

 @CurrentPage int,

 @TotalPage int output

BEGIN

 DECLARE @BeginID INT ,@EndID INT 

 SET @BeginId=@PageSize*@CurrentPage

 SET @EndID=@PageSize*(@CurrentPage+1)

 SELECT * FROM Product as p,##TmpProduct as t WHERE p.ProductId=t.ProductId and t.ProductId BETWEEN @BeginId AND 

@EndID

 SELECT @TotalPage=COUNT(*)/@PageSize FROM PRODUCT

  測試結果:

記錄總數           查詢時間(ms)

10000              10

100000            27

250000            41

500000            69

700000            86

  綜合兩種臨時表分頁方法分析,很明顯采用全局臨時表分頁的效率遠高於局部臨時表分頁,但是全局臨時表需要定時維護,包括記錄改變,

  索引改變。這種維護成本限制了該方法的發展。所以,在小數據量的情況下建議使用局部臨時表分頁,如果數據量較大請參考下面的方案。

  方案三:采用ROW_NUMBER()分頁

CREATE  PROCEDURE [dbo].[SelectProduct]

 @PageSize int,

 @CurrentPage int,

 @TotalPage int output

BEGIN

 DECLARE @BeginID INT ,@EndID INT 

 SET @BeginId=@PageSize*@CurrentPage

 SET @EndID=@PageSize*(@CurrentPage+1)

 SELECT * FROM (SELECT *,ROW_NUMBER()OVER(order by ProductId) AS ROWNUM FROM Product) as t  WHERE ROWNUM  BETWEEN 

@BeginId AND @EndID

 SELECT @TotalPage=COUNT(*)/@PageSize FROM Product 

  測試結果:

記錄總數           查詢時間(ms)

10000            210

100000          150

250000          165

500000           69

700000           86

  從統計數據中不難看出

  綜合所有分頁我們發現,分頁無非就是對記錄的編號進行處理,如果編號符合我們要求的我們就可以用連續表的方式直接使用,如果不符合

  要求的,我們便改變這種編號使其符合要求,如重新建立編號,其實臨時表和ROW_NUMBER()均屬於重建編號的過程。

  備注:文章中的數據均親自實測得來,該數據僅作參考和比較,不同的目標機運行時間都將不同。如果文章中存在不正確的觀點和看法,望

  大家指出,不能誤導讀者。請大家尊重筆者的勞動果實,轉載望注明出處:http://www.cnblogs.com/4inwork。當明白了存儲過程分頁的原

  理子後下篇文章將結合控件來一個具體事例。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved