建 立一个 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(1, 1), 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万页。

关于SQL分页存储过程的分析的更多相关文章

  1. [转]关于SQL分页存储过程的分析

    [转]关于SQL分页存储过程的分析 建立一个 Web 应用,分页浏览功能必不可少.这个问题是数据库处理中十分常见的问题.经典的数据分页方法是:ADO 纪录集分页法,也就是利用ADO自带的分页功能(利用 ...

  2. Delphi调用SQL分页存储过程实例

    Delphi调用SQL分页存储过程实例 (-- ::)转载▼ 标签: it 分类: Delphi相关 //-----下面是一个支持任意表的 SQL SERVER2000分页存储过程 //----分页存 ...

  3. 完整SQL分页存储过程(支持多表联接)

    http://www.cnblogs.com/andiki/archive/2009/03/24/1420289.html Code/********************************* ...

  4. 真正通用的SQL分页存储过程

    关于SQL分页的问题,网上找到的一些SQL其实不能真正做到通用,他们主要是以自增长ID做为前提的.但在实际使用中,很多表不是自增长的,而且主键也不止一个字段,其实我们稍做改进就可以达到通用.这里还增加 ...

  5. SQL - 分页存储过程

    http://www.jb51.net/article/71193.htm http://www.webdiyer.com/utils/spgenerator/ create PROCEDURE [d ...

  6. 修改后的SQL分页存储过程,利用2分法,支持排序

    /****** Object: StoredProcedure [dbo].[sys_Page_v3] Script Date: 08/13/2014 09:32:28 ******/ SET ANS ...

  7. sql分页存储过程比较

    一,先创建一百万条数据 drop table #tmp create table #tmp ( id ,) primary key, name ) ) declare @i int begin ins ...

  8. MS SQL 分页存储过程

    最近换了家新公司,但是新公司没有使用分页的存储过程.那我就自个写一个往项目上套 (效率怎么样就不怎么清楚没有详细的测试过) CREATE PROCEDURE [dbo].[pro_common_pag ...

  9. sql分页存储过程,带求和、排序

    创建存储过程: CREATE PROCEDURE [dbo].[sp_TBTest_Query] ( @PageSize INT, --每页多少条记录 @PageIndex INT = 1, --指定 ...

随机推荐

  1. hive查询不加分区的一个异常

    今天下午有同事反馈她提交了了一个SQL后,hive 查询就停止响应了. 我看了下,发现hiveserver确实hug住了.听过查看日志,发现了一个牛逼的SQL, 这个SQL很简单: select a. ...

  2. 实现echarts内外圈联动

    //控制都是通过控制series中data的name,那么将内外圈需要同事控制的部分设置为一样的名字,就可以实现内外圈联动. //但是在name相同时,会使默认分配颜色时相同,使颜色不好看,这里就需要 ...

  3. vue滚动行为

    有人问道如何记录vue页面的滚动条位置,再次载入组件的时候页面滚动到记录的位置? 思路: 记录滚动条位置我们好记 我们要在组件销毁之前也就是页面跳转的时候 需要用到生命周期beforeDistory将 ...

  4. caioj 1618 【动态规划】矩阵相乘的次数

    刷刷水题压压惊 低级版的能量项链 相当于复习一次中链式dp 这种合并了之后又后效性的题目 都可以用类似的方法做 #include<cstdio> #include<cstring&g ...

  5. Unity调用Android的两种方式:其一、调用jar包

    unity在Android端开发的时候,免不了要调用Java:Unity可以通过两种方式来调用Android:一是调用jar.二是调用aar. 这篇文章主要讲解怎么从无到有的生成一个jar包,然后un ...

  6. 【SRM 717 div2 A】 NiceTable

    Problem Statement You are given a vector t that describes a rectangular table of zeroes and ones. Ea ...

  7. Spring 容器(一)

    Source null instanceof Object 语法是正确的 Source定义得到输入流的方法 Resource继承Source,定义判断流是否存在.是否打开等方法 BaseResourc ...

  8. ArcGIS api for javascript——显示一个信息窗口

    描述 这个示例展示了在用户单击地图时如何在InfoWindow中显示信息.信息窗口是一个dijit (Dojo widget).信息窗口能够包含文本,字符,图片和任何通过HTML表示的事物.这个例子在 ...

  9. 算法导论————KMP

    [例题传送门:caioj1177] KMP模版:子串是否出现 [题意]有两个字符串SA和SB,SA是母串,SB是子串,问子串SB是否在母串SA中出现过.如果出现过输出第一次出现的起始位置和结束位置,否 ...

  10. Gym - 100685F Flood BFS

    Gym - 100685F 题意:n个水池之间流水,溢出多少流出多少,多个流出通道的话平均分配,给你每个水池中的水量和容量,问到最后目标水池中水量. 思路:直接用队列扩展,不过这里有一个优化,就是统计 ...