关于SQL分页存储过程的分析

建立一个 Web 应用,分页浏览功能必不可少。这个问题是数据库处理中十分常见的问题。经典的数据分页方法是:ADO 纪录集分页法,也就是利用ADO自带的分页功能(利用游标)来实现分页。但这种分页方法仅适用于较小数据量的情形,因为游标本身有缺点:游标是存放在内存中,很费内存。游标一建立,就将相关的记录锁住,直到取消游标。游标提供了对特定集合中逐行扫描的手段,一般使用游标来逐行遍历数据,根据取出数据条件的不同进行不同的操作。而对于多表和大表中定义的游标(大的数据集合)循环很容易使程序进入一个漫长的等待甚至死机。

更重要的是,对于非常大的数据模型而言,分页检索时,如果按照传统的每次都加载整个数据源的方法是非常浪费资源的。现在流行的分页方法一般是检索页面大小的块区的数据,而非检索所有的数据,然后单步执行当前行。

 

 最早较好地实现这种根据页面大小和页码来提取数据的方法大概就是“俄罗斯存储过程”。这个存储过程用了游标,由于游标的局限性,所以这个方法并没有得到大家的普遍认可。

 后来,网上有人改造了此存储过程,下面的存储过程就是结合我们的办公自动化实例写的分页存储过程:

 /************************************************************

 * Code formatted by SoftTree SQL Assistant ?v5.0.97
 * Time: 2011-10-05 20:32:46
 *********************************************************** */

CREATE  PROCEDURE page_t(
     @pagesize    INT,     -- 页面大小,如每页存储20条记录
     @pageindex   INT  -- 当前页码
)
AS
     SET NOCOUNT  ON
    
     BEGIN
         DECLARE  @indextable       TABLE(id  INT  IDENTITY( 11), nid  INT-- 定义表变量
         DECLARE  @PageLowerBound   INT  -- 定义此页的底码
         DECLARE  @PageUpperBound   INT  -- 定义此页的顶码
         SET  @PageLowerBound  = ( @pageindex  - 1*  @pagesize
         SET  @PageUpperBound  =  @PageLowerBound  +  @pagesize
         SET  ROWCOUNT  @PageUpperBound
         INSERT  INTO  @indextable
          (
            nid
          )
         SELECT id
         FROM   tt 
         -- where id >dateadd(day,-365,getdate()) order by id desc
         SELECT O.id
         FROM   tt O,
                @indextable t
         WHERE  O.id  = t.nid
                AND t.id  >  @PageLowerBound
                AND t.id  <=  @PageUpperBound
         ORDER  BY
               t.id
     END
    
     SET NOCOUNT  OFF
    

 

----exec page_t 100,2

 

 

以上存储过程运用了SQL SERVER的最新技术――表变量。应该说这个存储过程也是一个非常优秀的分页存储过程。当然,在这个过程中,您也可以把其中的表变量写成临时表:CREATE TABLE #Temp。但很明显,在SQL SERVER中,用临时表是没有用表变量快的。所以笔者刚开始使用这个存储过程时,感觉非常的不错,速度也比原来的ADO的好。但后来,我又发现了比此方法更好的方法。

 

 从tt表中取出第 n 条到第 m 条的记录:

 DECLARE @m INT 

DECLARE  @n  INT
SET  @m = 10000
set  @n = 2
SELECT  TOP ( @m - @n + 1*   FROM tt 
WHERE (id  NOT  IN  ( SELECT  TOP ( @n - 1) id    FROM tt))

id 为tt表的关键字

我当时看到这篇文章的时候,真的是精神为之一振,觉得思路非常得好。这个存储过程也是目前较为流行的一种分页存储过程

 /************************************************************

 * Code formatted by SoftTree SQL Assistant ?v5.0.97
 * Time: 2011-10-05 21:12:08
 *********************************************************** */

  CREATE   PROC pagination2(
                              @SQL  NVARCHAR( 4000),     -- 不带排序语句的SQL语句
                              @Page  INT,     -- 页码
                              @RecsPerPage  INT,     -- 每页容纳的记录数
                              @ID  VARCHAR( 255),     -- 需要排序的不重复的ID号
                              @Sort  VARCHAR( 255-- 排序字段及规则
                         )
AS

DECLARE  @Str  NVARCHAR( 4000)

SET  @Str  =  ' SELECT TOP  '  +  CAST( @RecsPerPage  AS  VARCHAR( 20))  +  '  * FROM 
(
'  +  @SQL  +  ' ) T WHERE T. '  +  @ID  +  '  NOT IN (SELECT TOP  '  +  CAST(( @RecsPerPage  * ( @Page  - 1))  AS  VARCHAR( 20))
     +  '   '  +  @ID  +  '  FROM ( '  +  @SQL  +  ' ) T9 ORDER BY  '  +  @Sort  +  ' ) ORDER BY  '  + 
     @Sort

PRINT  @Str

EXEC sp_ExecuteSql  @Str
GO


-- ---pagination2 'SELECT id FROM tt',2,100,'id','id' 


    

 其实,以上语句可以简化为:

 SELECT   TOP 100/*页大小*/*

FROM tt  WHERE (ID  NOT  IN ( SELECT  TOP  ( 100 /* 页大小 */  * 0 /* 页数 */ )id  FROM tt /* */  ORDER  BY id))
ORDER  BY ID

 

 

但这个存储过程有一个致命的缺点,就是它含有NOT IN字样。虽然我可以把它改造为:

 

SELECT  TOP  100 /* 页大小  */  *
FROM   TT a
WHERE   NOT  EXISTS
       (
            SELECT  *
            FROM   (
                       SELECT  TOP( 100 /* 页大小  */  *  0 /* 页数 */* 
                       FROM   TT
                       ORDER  BY
                             id
                  ) b
            WHERE  b.id  = a.id
       )

ORDER BY id  

 

即,用not exists来代替not in,但我们前面已经谈过了,二者的执行效率实际上是没有区别的。既便如此,用TOP 结合NOT IN的这个方法还是比用游标要来得快一些。

 

虽然用not exists并不能挽救上个存储过程的效率,但使用SQL SERVER中的TOP关键字却是一个非常明智的选择。因为分页优化的最终目的就是避免产生过大的记录集,而我们在前面也已经提到了TOP的优势,通过TOP 即可实现对数据量的控制。

在分页算法中,影响我们查询速度的关键因素有两点:TOP和NOT IN。TOP可以提高我们的查询速度,而NOT IN会减慢我们的查询速度,所以要提高我们整个分页算法的速度,就要彻底改造NOT IN,同其他方法来替代它。

 

我们知道,几乎任何字段,我们都可以通过max(字段)或min(字段)来提取某个字段中的最大或最小值,所以如果这个字段不重复,那么就可以利用这些不重复的字段的max或min作为分水岭,使其成为分页算法中分开每页的参照物。在这里,我们可以用操作符“>”或“<”号来完成这个使命,使查询语句符合SARG形式。如:

 

 

 

SELECT  TOP  10  * 
FROM   tt
WHERE  id  >  200

 

 

 于是就有了如下分页方案:


select  top 页大小  *
from tt
where id >
( select  max (id)  from 
( select  top ((页码 - 1) *页大小) id  from tt  order  by id)  as T

order  by id

 

 

 在选择即不重复值,又容易分辨大小的列时,我们通常会选择主键。下表列出了笔者用有着1000万数据的办公自动化系统中的表,在以id(id是主键,但并不是聚集索引。)为排序列、提取id字段,分别以第1、10、100、500、1000、1万、10万、25万、50万页为例,测试以上三种分页方案的执行速度:(单位:毫秒)

页码 方案1 方案2 方案3
1 60 30 76
10 46 16 63
100 1076 720 130
500 540 12943 83
1000 17110 470 250
10000 24796 4500 140
100000 38326 42283 1553
250000 28140 128720 2330
500000 121686 127846 7168

从上表中,我们可以看出,三种存储过程在执行100页以下的分页命令时,都是可以信任的,速度都很好。但第一种方案在执行分页1000页以上后,速度就降了下来。第二种方案大约是在执行分页1万页以上后速度开始降了下来。而第三种方案却始终没有大的降势,后劲仍然很足。

 

 在确定了第三种分页方案后,我们可以据此写一个存储过程。大家知道SQL SERVER的存储过程是事先编译好的SQL语句,它的执行效率要比通过WEB页面传来的SQL语句的执行效率要高。下面的存储过程不仅含有分页方案,还会根据页面传来的参数来确定是否进行数据总数统计。

获取指定页的数据:

 /************************************************************

 * Code formatted by SoftTree SQL Assistant ?v5.0.97
 * Time: 2011-10-05 21:40:08
 *********************************************************** */

CREATE  PROCEDURE PAGE01
     @tblName  VARCHAR( 255),  --  表名
     @strGetFields  VARCHAR( 1000=  ' * '--  需要返回的列
     @fldName  VARCHAR( 255=  ''--  排序的字段名
     @PageSize  INT  =  10--  页尺寸
     @PageIndex  INT  =  1--  页码
     @doCount  BIT  =  0--  返回记录总数, 非 0 值则返回
     @OrderType  BIT  =  0--  设置排序类型, 非 0 值则降序
     @strWhere  VARCHAR( 1500=  ''''  --  查询条件 (注意: 不要加 where)
AS
     DECLARE  @strSQL     VARCHAR( 5000--  主语句
     DECLARE  @strTmp     VARCHAR( 110--  临时变量
     DECLARE  @strOrder   VARCHAR( 400--  排序类型
     IF  @doCount  !=  0
     BEGIN
         IF  @strWhere  !=  ''''
             SET  @strSQL  =  ' select count(*) as Total from '  +  '   '  +  @tblName  + 
                 '  where '  +  @strWhere
         ELSE
             SET  @strSQL  =  ' select count(*) as Total from  '  +  @tblName
     END -- 以上代码的意思是如果@doCount传递过来的不是0,就执行总数统计。以下的所有代码都是@doCount为0的情况:
     ELSE
     BEGIN
         IF  @OrderType  !=  0
         BEGIN
             SET  @strTmp  =  ' <(select min '
             SET  @strOrder  =  '  order by  '  +  @fldName  +  '  desc '
                 -- 如果@OrderType不是0,就执行降序,这句很重要!
         END
         ELSE
         BEGIN
             SET  @strTmp  =  ' >(select max '
             SET  @strOrder  =  '  order by  '  +  @fldName  +  '  asc '
         END
         IF  @PageIndex  =  1
         BEGIN
             IF  @strWhere  !=  ''''
                 SET  @strSQL  =  ' select top  '  +  STR( @PageSize+  '   '  +  @strGetFields
                     +  '
                 from 
'  +  @tblName  +  '  where  '  +  @strWhere  +  '   '  +  @strOrder
             ELSE
                 SET  @strSQL  =  ' select top  '  +  STR( @PageSize+  '   '  +  @strGetFields
                     +  '  
           from 
'  +  @tblName  +  '   '  +  @strOrder
                     -- 如果是第一页就执行以上代码,这样会加快执行速度
         END
         ELSE
         BEGIN
             -- 以下代码赋予了@strSQL以真正执行的SQL代码 
             SET  @strSQL  =  ' select top  '  +  STR( @PageSize+  '   '  +  @strGetFields  + 
                 '  from  '
                 +  @tblName  +  '  where  '  +  @fldName  +  ''  +  @strTmp  +  ' ( '  +  @fldName 
                 +  '
          from (select top 
'  +  STR(( @PageIndex  - 1*  @PageSize+  '   '  +  @fldName 
                 +  '  
          from 
'  +  @tblName  +  ''  +  @strOrder  +  ' ) as tblTmp) '  +  @strOrder
            
             IF  @strWhere  !=  ''''
                 SET  @strSQL  =  ' select top  '  +  STR( @PageSize+  '   '  +  @strGetFields
                     +  '  from [ '
                     +  @tblName  +  ' ] where [ '  +  @fldName  +  ' ] '  +  @strTmp  +  ' ([ '
                     +  @fldName  +  ' ]) from (select top  '  +  STR(( @PageIndex  - 1*  @PageSize
                     +  '  [ '
                     +  @fldName  +  ' ] from [ '  +  @tblName  +  ' ] where  '  +  @strWhere 
                     +  '   '
                     +  @strOrder  +  ' ) as tblTmp) and  '  +  @strWhere  +  '   '  +  @strOrder
         END
     END 
     EXEC ( @strSQL)
GO


  -- -----PAGE01 'tt','*','id'

 

 

 

/* ***********************************************************
 * Code formatted by SoftTree SQL Assistant ?v5.0.97
 * Time: 2011-10-05 21:35:12
 ***********************************************************
*/

CREATE  PROCEDURE PAGE1_tt
     @tblName  VARCHAR( 255),  --  表名
     @strGetFields  VARCHAR( 1000=  ' * '--  需要返回的列
     @fldName  VARCHAR( 255=  ''--  排序的字段名
     @PageSize  INT  =  10--  页尺寸
     @PageIndex  INT  =  1--  页码
     @doCount  BIT  =  0--  返回记录总数, 非 0 值则返回
     @OrderType  BIT  =  0--  设置排序类型, 非 0 值则降序
     @strWhere  VARCHAR( 1500=  ''''  --  查询条件 (注意: 不要加 where)
AS
     DECLARE  @strSQL     VARCHAR( 5000--  主语句
     DECLARE  @strTmp     VARCHAR( 110--  临时变量
     DECLARE  @strOrder   VARCHAR( 400--  排序类型
     IF  @doCount  !=  0
     BEGIN
         IF  @strWhere  !=  ''''
             SET  @strSQL  =  ' select count(*) as Total from '  +  '   '  +  @tblName  + 
                 '  where '  +  @strWhere
         ELSE
             SET  @strSQL  =  ' select count(*) as Total from  '  +  @tblName
     END -- 以上代码的意思是如果@doCount传递过来的不是0,就执行总数统计。以下的所有代码都是@doCount为0的情况:
     ELSE
     BEGIN
         IF  @OrderType  !=  0
         BEGIN
             SET  @strTmp  =  ' <(select min '
             SET  @strOrder  =  '  order by  '  +  @fldName  +  '  desc '
                 -- 如果@OrderType不是0,就执行降序,这句很重要!
         END
         ELSE
         BEGIN
             SET  @strTmp  =  ' >(select max '
             SET  @strOrder  =  '  order by  '  +  @fldName  +  '  asc '
         END
         IF  @PageIndex  =  1
         BEGIN
             IF  @strWhere  !=  ''''
                 SET  @strSQL  =  ' select top  '  +  STR( @PageSize+  '   '  +  @strGetFields
                     +  '
                 from 
'  +  @tblName  +  '  where  '  +  @strWhere  +  '   '  +  @strOrder
             ELSE
                 SET  @strSQL  =  ' select top  '  +  STR( @PageSize+  '   '  +  @strGetFields
                     +  '  
           from 
'  +  @tblName  +  '   '  +  @strOrder
                     -- 如果是第一页就执行以上代码,这样会加快执行速度
         END
         ELSE
         BEGIN
             -- 以下代码赋予了@strSQL以真正执行的SQL代码 
             SET  @strSQL  =  ' select top  '  +  STR( @PageSize+  '   '  +  @strGetFields  + 
                 '  from  '
                 +  @tblName  +  '  where  '  +  @fldName  +  ''  +  @strTmp  +  ' ( '  +  @fldName 
                 +  '
          from (select top 
'  +  STR(( @PageIndex  - 1*  @PageSize+  '   '  +  @fldName 
                 +  '  
          from 
'  +  @tblName  +  ''  +  @strOrder  +  ' ) as tblTmp) '  +  @strOrder
            
             IF  @strWhere  !=  ''''
                 SET  @strSQL  =  ' select top  '  +  STR( @PageSize+  '   '  +  @strGetFields
                     +  '  from [ '
                     +  @tblName  +  ' ] where [ '  +  @fldName  +  ' ] '  +  @strTmp  +  ' ([ '
                     +  @fldName  +  ' ]) from (select top  '  +  STR(( @PageIndex  - 1*  @PageSize
                     +  '  [ '
                     +  @fldName  +  ' ] from [ '  +  @tblName  +  ' ] where  '  +  @strWhere 
                     +  '   '
                     +  @strOrder  +  ' ) as tblTmp) and  '  +  @strWhere  +  '   '  +  @strOrder
         END
     END 
     EXEC ( @strSQL)

GO

 --PAGE1_tt   'tt','*','id',100,1,0

 

 上面的这个存储过程是一个通用的存储过程,其注释已写在其中了。 在大数据量的情况下,特别是在查询最后几页的时候,查询时间一般不会超过9秒;而用其他存储过程,在实践中就会导致超时,所以这个存储过程非常适用于大容量数据库的查询。 笔者希望能够通过对以上存储过程的解析,能给大家带来一定的启示,并给工作带来一定的效率提升,同时希望同行提出更优秀的实时数据分页算法。

以上的这第三种存储过程在小数据量的情况下,有如下现象:
1、分页速度一般维持在1秒和3秒之间。
2、在查询最后一页时,速度一般为5秒至8秒,哪怕分页总数只有3页或30万页。

 

 

你可能感兴趣的:(存储过程)