[转]关于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(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分页存储过程的分析

    建 立一个 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. 红豆带你从零学C#系列之:开始C#编程(一)

    让我们开始学习C#编程吧 作者:红豆西米露 交流QQ:937802080 前面的文章里给大家介绍了C#语言的一些基本认识,现在我们来开始做一个小程序吧! 在这里以我们以“控制台应用程序”来作演示. P ...

  2. BZOJ 2245: [SDOI2011]工作安排( 费用流 )

    费用流模板题..限制一下不同愤怒值的工作数就可以了. ------------------------------------------------------------------------- ...

  3. Android 应用接入广点通统计API 方案

    官方给你参考文档,很扯淡,是c++和python脚本: 安卓java代码接入如下: package com.edaixi.util; import java.io.UnsupportedEncodin ...

  4. 在实体对象中访问导航属性里的属性值出现异常“There is already an open DataReader associated with this Command which must be closed first”

    在实体对象中访问导航属性里的属性值出现异常“There is already an open DataReader associated with this Command which must be ...

  5. Tensorflow tflearn 编写RCNN

    两周多的努力总算写出了RCNN的代码,这段代码非常有意思,并且还顺带复习了几个Tensorflow应用方面的知识点,故特此总结下,带大家分享下经验.理论方面,RCNN的理论教程颇多,这里我不在做详尽说 ...

  6. Eclips入门教程

    1. 插件推荐 Eclipse默认情况下是一个纯净版的,所以功能简单,而开源IDE最为强大的莫过于各种插件,通过使用插件可以帮助我们减少大量编写代码的工作量,也帮助我们降低了编写代码的难度,所以懂得安 ...

  7. Zepto Api参考

    zepto API参考 简介 Zepto是一个轻量级的针对现代高级浏览器的JavaScript库, 它与jquery有着类似的api. 如果你会用jquery,那么你也会用zepto. 设计目的 ze ...

  8. Linux特殊权限:SUID、SGID、SBIT

    SUID:      只对二进制程序有效      执行者对于程序需要有x权限      在程序运行过程中,执行者拥有程序拥有者的权限      例如:      普通用户执行passwd命令.   ...

  9. pcre函数具体解释

    PCRE是一个NFA正则引擎,不然不能提供全然与Perl一致的正则语法功能.但它同一时候也实现了DFA,仅仅是满足数学意义上的正则. PCRE提供了19个接口函数,为了简介,使用PCRE内带的測试程序 ...

  10. xcode 工具栏中放大镜的替换的简单说明

    1.如果是在打开的文档范围内:       查找: Command+ F       替换: Option+Command+F                   Replace All   是全部替 ...