翻动 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 =  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 ) +  )
--定位
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 ) +  20 *( 3-1 ) +  41
正是因为ID是不连续的所以才需要用第一个语句来定位,如果是连续的那还用第一条语句做什么呢?
举各少量数据的例子:
假设有
10 条记录,ID是: 1000 500 320 205 115 110 95 68 4 1 。这回不写连续的了免的误会
一页显示两条记录,现在要显示第三页,那么第三页的id就是 
115 110
先看第一条语句
select  top  @id=ID  from table order by  ID  desc
不知道大家有没有看懂这句,这时print @id 得到的结果是  115
再看第二条语句
select  top  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