翻動 100 萬級的數據?——?只需幾十毫秒?之揭秘篇:有詳細的說明,不要錯過。
感謝大家的支持!!!
昨天發了一個邀請,邀請大家幫忙測試,效果還可以,下面小結一下:
通過內部的計數器得知:訪問次數是
1071 (其中有好多是自己點的:)),人數不是太理想,本來是想看看上萬人同時訪問的情況:)
系統資源的占用情況
內存?——?很理想。
SQL 占用的內存最大也沒有超過 65 M,一般是在 35 M左右;asp.net占用的內存最大也沒有超過 40 M,一般是在 25 M左右。
CPU:
8 %左右,由于訪問次數不多,也不夠集中,所以這個數值也說明不了什么。自己連續點了n次下一頁,發現CPU的使用率飄高,達到了 50 %左右。
但是對于
100 萬的記錄,AMD?XP2000+?的CPU?幾十毫秒的放映速度,因該是可以接受的,甚至是很理想的吧。
畢竟服務器的CPU要比我的快很多吧,而且記錄也很難達到
100 萬吧。
結果還是很滿意的,但是美中不足的是,我想看一下海量訪問的情況下的效果,
希望大家再支持一下,多點幾下,謝謝了。呵呵
另外說明一下:前n頁可以在
60 毫秒內完成,n應該是大于 500 的,小于多少嘛還沒有測試。后n頁就比較慢了,需要 500 毫秒左右。
下面討論一下翻頁的技巧吧。
我沒有用游標、臨時表、
not?in in? 這些方法,并不是說他們的效率不高,而是我還沒有測試過。我只用了?top?,查了兩次表。
大家也可提供一些其他的方法,我來測試一下,看看在
100 萬條的情況下的效果。(請不要給在存儲過程里面組串的,看著實在是太費勁了)

討論的前提是在海量數據的情況下,至少是在
10 萬以上的。如果是很少的數據呢,那怎么翻都可以了。也差不了多少。
1. 設置合理的索引
首先要做的是設置合理的索引,這個好像經常被忽略,至少很少被談起。
注意:主鍵是索引的一種,而且是最快的一種。如果你都是把主鍵當作排序字段的話,那么你已經利用了索引。
不設置合理的索引的話,會導致查詢速度非常的慢,甚至會造成超時。
這方面你可以做一個實驗:找一個表,填進去
10 萬條記錄,假設有ID?、addedDate等字段,在查詢分析器里面執行一下
select? top? 10? *? from?table
應該立刻就能出現結果。
然后再執行?
select? top? 10? *? from?table?order?by? ID(這時ID字段是主鍵)
也是立刻就出現了結果。
然后再執行?
select? top? 10? *? from?table?order?by? addedDate?(這時addedDate字段沒有索引)
你會發現速度很慢。
現在給addedDate?加一個非聚集索引,然后在執行上面的查詢語句,速度也變得很快了。
可見索引神奇的效果!
這是翻動百萬級記錄最基本的設置,具體到我的那個論壇的翻頁,我是設置了BoardID、?replyDate兩個字段作為聯合索引的。
因為是要在同一個討論組李翻頁,而且是按replyDate排序的。

2. 只返回需要的記錄
對于海量數據,都讀出來做緩存,那是不可想象的(記錄少的話,也要看利用率,一般都是很浪費的)。
所以呢如果一頁顯示
20 條的話名那就只都讀出來 20 條,這樣就很省內存和時間。
注意:雖然ADO.NET里面有這個方法
SqlDataAdapter.Fill(DataSet1,startRecord,maxRecords,srcTable);
但是他還是要先從
SQL 里面把查詢語句的查出來的所有記錄都出來,然后在截取指定的記錄數。這對于 SQL 來說是一樣的,對于海量數據依然會很慢。
論壇里的首頁用的是
select? top? 20? *? from?table?where? boardID?=? 5? order?by? replyDate? desc
這樣呢就只返回了 20 條記錄,再加上索引的功勞,速度是非常快的。
?
3. 盡量減少字段的長度
一個表可以建很多的字段,但是字段的總長度不能超過
8060 B,也就是說如果你建了一個 char ( 8060 )的字段,就不能在建其他的字段了。
我在第一次的測試中(星期天的),把主題的所有信息都放在了一個表里面,包括了一個nvarchar(
3600 )的主題內容的字段,復制記錄的時候發現非常的慢,
當達到
9 萬的時候,就已經很慢的,勉強把記錄數拷貝到了 35 萬,加了索引,測試了一下,翻頁速度還是可以的,前n也都是很快的,后n頁就很慢了,
如果再加上查詢那就非常之慢了。
查看了一下數據文件嚇了一跳?——?他居然占用了
1.4 G的硬盤空間,怪不得拷貝和查詢都慢的要死呢。
于是修改了一下表結構,把那個nvarchar(
3600 )的主題內容的字段踢了出去,放在一個單獨的表里面。
再重新拷貝記錄就非常的快了,很快就把記錄數從
16 表成了 1048577 。昨天的測試就是在這個條件下進行的。
4. 技巧
終于到了翻頁算法的地方了,呵呵沒有等急吧。
思路呢就是先找到一個標志,然后呢把大于(或小于)這個標志的前n條記錄取出來。
什么?沒看懂。沒關系,我舉個例子吧。
假設是按ID倒序的,每一頁顯示
10 條記錄,有 100 條記錄,記錄號正好是 1 100 (怎么這么巧??為了說明方便嘛)
那么第一頁的記錄就是
100 91 、第二頁的記錄就是 90 81 、第三頁的記錄就是 80 71......
我現在要翻到第三頁,那么要找到第 21 行的記錄的ID的值(也就是 80 ),然后把小于等于 80 的記錄用top? 10? 取出來就行了。
查詢語句
declare? @pageSize? int? --返回一頁的記錄數
declare? @CurPage? int? --頁號(第幾頁)1:第一頁;2:第二頁;......;-1最后一頁。
declare? @Count? int
declare?
@id? int
set?
@pageSize= 10
set? @CurPage?= 1
if? @CurPage?=?- 1
begin
--最后一頁
set? rowcount?@pageSize
select? @id=ID? from?table?order?by? ID
end
--定位
if? @CurPage?>? 0
begin
set?
@Count?=?@pageSize?*?(@CurPage?- 1 )?+? 1
set? rowcount?@Count
select? @id=ID? from?table?order?by? ID? desc
end
--返回記錄
set? rowcount?@pageSize
select? *? from?table?where? ID?<=@id? order?by? ID? desc
set?
rowcount? 0

其中“定位”用了? select? @id=ID? from?table?order?by? ID? desc
這種方法,感覺上是很省內存的,因為只記錄了一個ID,
然后用?
select? *? from?table?where? ID?<=@id? order?by? ID? desc
取得最終需要的記錄
set? rowcount?@pageSize?相當于?top?@pageSize?。

優點:無論翻到哪一頁,內存的占用情況都不變,多人訪問內存也不會不變,很多人呢,還沒有測試出來:)
缺點:單表、單排序字段。
?
?
http://community.csdn.net/Expert/TopicView3.asp?id=
4182510
發了這個帖子,回復的人很多,感謝大家的支持。這里有個誤會我不得不說明一下,免的誤人子弟。
在帖子里我并不是寫了個算法就完事了,而是說了很多翻動海量數據要注意的地方,
比如建立合理的索引,只返回需要的記錄?,盡量減少字段的長度?等注意到或沒有注意到的地方。
最后說的才是算法,可能是我的表達能力太差了吧,舉的例子給大家帶來了誤會。
翻頁的語句?(?@pageSize?*?(@CurPage?-
1 )?+? 1? )
--定位
declare? @id? int
select?
top? 41? @id=ID? from?table?order?by? ID? desc
--顯示數據
select? top? 20? *? from?table?where? ID?<=@id? order?by? ID? desc
按照ID倒序排列(也就是按照 int 類型的字段排序)
一頁顯示
20 條記錄,這是顯示第三頁的語句
@pageSize?*?(@CurPage?-
1 )?+? 1? =? 20 *( 3-1 )?+? 1? =? 41
正是因為ID是不連續的所以才需要用第一個語句來定位,如果是連續的那還用第一條語句做什么呢?
舉各少量數據的例子:
假設有
10 條記錄,ID是: 1000 500 320 205 115 110 95 68 4 1 。這回不寫連續的了免的誤會
一頁顯示兩條記錄,現在要顯示第三頁,那么第三頁的id就是?
115 110
先看第一條語句
select? top? 5? @id=ID? from?table?order?by? ID? desc
不知道大家有沒有看懂這句,這時print?@id?得到的結果是? 115
再看第二條語句
select? top? 2? *? from?table?where? ID?<= 115? order?by? ID? desc
這時的記錄集就是? 115 110 ,也就是我們所需要的記錄了。

注意:不需要連續的ID,也不局限只能按ID排序,你可以換成ReplyDate(最后回復時間)字段,
當然了
declare? @id? int? 要改成? declare? @id?datetime
這里的ID?是主鍵,唯一標識記錄的字段,它本身就是一種索引,而且是效率最高的索引。
A.唯一標識記錄的字段的值怎么能隨意改動呢,那不亂套了嗎?
B.主鍵是最快的索引,可能你還沒有意識到(一開始我就不知道,學了
SQL 很久以后才知道的),如果你的算法用它作為排序字段,那么速度會很快,會比用其他字段(沒有索引的字段)排序快很多。
C.用ReplyDate(最后回復時間)來排序,那么就必須給他建立索引(在海量數據的情況下),否則會超時的。

D.建立索引后,再執行添加、修改、刪除會對數據庫帶來災難性的折磨??
一開始我也是這么認為的,但是為了能夠翻頁,不得不加索引。
但是接下來的事實確打消了我的顧慮
先來看添加。
100 萬條記錄是怎么弄出來的?大家可以看到帖子里有很多標題一樣的主題,對了是復制出來的。
我先加了
16 條記錄,然后加上了索引。注意在 insert?into? 之前就已經建立好了索引!
接下來就是
insert?into?table? (...)? select? ...? from?table? 影響的行數:
16 32 64 128 256 512 1024 2048 4096 8192 16384 32768 65536
131072 262144 524288? 很快記錄就達到了 100 完了。
最后一次也只不過一兩分鐘(具體的時間忘記了,反正是很快了)。
同時,論壇也提供了發貼的功能,只是在批量添加記錄的時候,把一些記錄的最后回復時間弄成了
2006 年,
所以,你發的帖子不會顯示在第一頁。但是你可以看到,執行時間是很快的。
可見添加的時候是不成問題的,索引是倒序排列的,所以影響的行數絕對沒有你想象的那么多。
再來看修改。
看了sp1234的回復,加了修改的功能,只是為了測試,所以呢可以修改標題、最后發表時間、分組ID。
為什么可以修改這幾個字段呢?標題是普通字段,最后發表時間和分組ID是索引字段。
修改這幾個字段需要的時間都是很快的,在最后回復時間的右面有?[改]?[刪]?字樣,大家可以試一試。
同樣,修改的時候,影響的行數也不是很多。
最后看刪除
不多說了,論壇提供了這個功能,試一下就知道了。另外,刪除的時候,不用重新建立一遍索引吧?

在來說一下使用范圍吧。
首先呢這只是一種方法,而不是一個通用的存儲過程,也就是說要根據情況作適當的修改。
最佳使用環境:
單表,單排序字段,可以利用索引。
注意事項:
排序字段不必連續,最好使用
int 、datetime類型的字段,字符串型的字段沒有試過,效果可能會略差。
表可以沒有主鍵,但是對于海量數據的情況下,必須建立合理的索引。
有一個比較致命的限制,大家好像都沒有發現,那就是排序字段的重復性,
最好是沒有重復的,但不是說絕對不能有重復的記錄,有不要緊,只要不跨頁就行,跨頁的話就會擠掉若干條記錄,
用時間字段來排序,發生重復的記錄的可能性就很小了。

擴展性:
bingbingcha(不思不歸,不孟不E,原來是頭大灰狼)?的回復很精彩
-----------------
這樣的技巧在 SQL 區都討論過了..速度是很快的..但是滿足不了需求的..實用性太差了..現在的企業需要用到分頁的大部分都是多表查詢..單表分頁滿足不了需求的..
這個存儲過程可以擴展..用臨時表+樓主的方法..是個不錯的選擇..
-----------------
對于多表關聯查詢,有兩種方法,第一種就是bingbingcha說的?——?“用臨時表+樓主的方法”,這是在海量數據的時候唯一可行的方法。
但是在小數據量的時候,這么些就有一點繁瑣,而且不容易歸納到通用的寫法里。
先來看一下查詢語句據的寫法:
聯合的
SELECT? a.ReplyID,?a.TopicID
FROM? dbo.BBS_Reply?a? INNER?JOIN
dbo.BBS_body?b? ON? a.BodyID?=?b.bodyID
where? a.ReplyID?> 10
單表的
SELECT? ReplyID,?TopicID
FROM? dbo.BBS_Reply
where? ReplyID?> 10
有沒有看到相同的地方:
select? 顯示的字段
from?
where? 條件
那么單表查詢和多表查詢有什么區別呢?
至少有很多的多表(單字段排序)查詢都是可用這種方式的。
注意:我并沒有說所有的多表(單字段排序)查詢都可以用,看具體情況了。
?
這是一個效率最高(需要合理的索引的幫忙),比較通用的翻頁方法。不知道這次我有沒有講明白。
==============================================================================
CREATE?PROCEDURE? CN5135_SP_Pagination
/*
***************************************************************
**?千萬數量級分頁存儲過程?**
***************************************************************
參數說明:
1.Tables?:表名稱,視圖
2.PrimaryKey?:主關鍵字
3.Sort?:排序語句,不帶Order?By?比如:NewsID?Desc,OrderRows?Asc
4.CurrentPage?:當前頁碼
5.PageSize?:分頁尺寸
6.Filter?:過濾語句,不帶Where
7.Group?:Group語句,不帶Group?By
效果演示:http://www.cn5135.com/_App/Enterprise/QueryResult.aspx
***************************************************************/
(
@Tables?
varchar ( 1000 ),
@PrimaryKey?
varchar ( 100 ),
@Sort?
varchar ( 200 )?=? NULL ,
@CurrentPage?
int? =? 1 ,
@PageSize?
int? =? 10 ,
@Fields?
varchar ( 1000 )?=? '*' ,
@Filter?
varchar ( 1000 )?=? NULL ,
@
Group?varchar ( 1000 )?=? NULL
)
AS
/*默認排序*/
IF? @Sort? IS?NULL?OR? @Sort?=? ''
SET? @Sort?=?@PrimaryKey
DECLARE? @SortTable? varchar ( 100 )
DECLARE? @SortName? varchar ( 100 )
DECLARE? @strSortColumn? varchar ( 200 )
DECLARE? @operator? char ( 2 )
DECLARE? @type? varchar ( 100 )
DECLARE? @prec? int
/*設定排序語句.*/
IF? CHARINDEX( 'DESC' ,@Sort)> 0
BEGIN
SET?
@strSortColumn?=?REPLACE(@Sort,? 'DESC' ,? '' )
SET? @operator?=? '<='
END
ELSE
BEGIN
IF?
CHARINDEX( 'ASC' ,?@Sort)?=? 0
SET? @strSortColumn?=?REPLACE(@Sort,? 'ASC' ,? '' )
SET? @operator?=? '>='
END
IF?
CHARINDEX( '.' ,?@strSortColumn)?>? 0
BEGIN
SET?
@SortTable?=?SUBSTRING(@strSortColumn,? 0 ,?CHARINDEX( '.' ,@strSortColumn))
SET? @SortName?=?SUBSTRING(@strSortColumn,?CHARINDEX( '.' ,@strSortColumn)?+? 1 ,?LEN(@strSortColumn))
END
ELSE
BEGIN
SET?
@SortTable?=?@Tables
SET? @SortName?=?@strSortColumn
END
SELECT?
@type=t.name,?@prec=c.prec
FROM? sysobjects?o
JOIN? syscolumns?c? on? o.id=c.id
JOIN? systypes?t? on? c.xusertype=t.xusertype
WHERE? o.name?=?@SortTable? AND? c.name?=?@SortName
IF? CHARINDEX( 'char' ,?@type)?>? 0
SET? @type?=?@type?+? '('? +? CAST (@prec? AS?varchar )?+? ')'
DECLARE? @strPageSize? varchar ( 50 )
DECLARE? @strStartRow? varchar ( 50 )
DECLARE? @strFilter? varchar ( 1000 )
DECLARE? @strSimpleFilter? varchar ( 1000 )
DECLARE? @strGroup? varchar ( 1000 )
/*默認當前頁*/
IF? @CurrentPage?<? 1
SET? @CurrentPage?=? 1
/*設置分頁參數.*/
SET? @strPageSize?=? CAST (@PageSize? AS?varchar ( 50 ))
SET? @strStartRow?=? CAST (((@CurrentPage?-? 1 )*@PageSize?+? 1 )? AS?varchar ( 50 ))
/*篩選以及分組語句.*/
IF? @Filter? IS?NOT?NULL?AND? @Filter?!=? ''
BEGIN
SET?
@strFilter?=? '?WHERE?'? +?@Filter?+? '?'
SET? @strSimpleFilter?=? '?AND?'? +?@Filter?+? '?'
END
ELSE
BEGIN
SET?
@strSimpleFilter?=? ''
SET? @strFilter?=? ''
END
IF?
@ Group?IS?NOT?NULL?AND? @ Group? !=? ''
SET? @strGroup?=? '?GROUP?BY?'? +?@ Group? +? '?'
ELSE
SET?
@strGroup?=? ''
/*執行查詢語句*/
EXEC (
'
DECLARE?@SortColumn?'?
+?@type?+? '
SET?ROWCOUNT?'?
+?@strStartRow?+? '
SELECT?@SortColumn='?
+?@strSortColumn?+? '?FROM?'? +?@Tables?+?@strFilter?+? '?'? +?@strGroup?+? '?ORDER?BY?'? +?@Sort?+? '
SET?ROWCOUNT?'?
+?@strPageSize?+? '
SELECT?'?
+?@Fields?+? '?FROM?'? +?@Tables?+? '?WHERE?'? +?@strSortColumn?+?@operator?+? '?@SortColumn?'? +?@strSimpleFilter?+? '?'? +?@strGroup?+? '?ORDER?BY?'? +?@Sort?+? '
'
)
GO