数据库分页是老生常谈的问题了。如果使用ORM框架,再使用LINQ的话,一个Skip和Take就可以搞定。但是有时由于限制,需要使用存储过程来实现。在SQLServer中使用存储过程实现分页的已经有很多方法了。之前在面试中遇到过这一问题,问如何高效实现数据库分页。刚好上周在业务中也遇到了这个需求,所以在这里简单记录和分享一下。

一 需求

这里以SQLServer的示例数据库NorthWind为例,里面有一张Product表,现在假设我们的需求是要以UnitPrice降序排列,并且分页,每一页10条记录。要求服务端分页。参数为每页记录数和页码。

二 实现

Top分页

当时采用的最直接做法就是使用两个Top来实现, 最后返回的结果是升序的,在C#代码里再处理一下就可以了。 这里作为演示,语句中使用 * 为了方便,实际开发中要替换为具体的列名。下面的方法简单吧。

SELECT TOP (@pageSize)
        *
FROM    ( SELECT TOP ( @pageSize * @pageIndex )
                    *
          FROM      [Northwind].[dbo].[Products]
          ORDER BY  UnitPrice DESC
        ) AS product
ORDER BY product.UnitPrice 

但是这个代码是有问题的,不知道各位发现了没有。当符合条件的纪录集小于每页记录数时,没有问题,但是当大于就有问题了,比如,在实例数据库中Products中有 77 条记录,当每页10条记录,第8页只应该返回7条记录,第9页应该返回空,但是使用如上的方法,每次都会返回10条记录。

沿用上面的思路,把代码修改为了如下采用三层Select,最内一层查询所有记录之前的数据,然后第二层选择Top PageSize个所有NOT IN 第一层数据中的数据即可,因为使用了NOT IN所以不存在第一种方法中的bug

SELECT  *
FROM    dbo.Products
WHERE   ProductID IN (
        SELECT TOP ( @pageSize )
                ProductID
        FROM    dbo.Products
        WHERE   ProductID NOT IN ( SELECT TOP ( @pageSize * (@pageIndex-1) )
                                            ProductID
                                   FROM     dbo.Products
                                   ORDER BY UnitPrice DESC )
        ORDER BY dbo.Products.UnitPrice DESC )
ORDER BY dbo.Products.UnitPrice ASC

使用ROW_NUMBER 函数分页

其实还有一种最简单最直接的思路,那就是采用临时表,即在内存中创建一个表变量,该变量中包含一个自增列,表关键字列,然后将待排序的表按照排序条件和规则插入到这张表中,然后就可以将自增列作为行号使用了,在比较早的如SQLServer 2000中,只能这样做,但是对于大数据量的记录集,需要创建的临时表也比较大,效率比较低,这里就不介绍了。

在SQLServer2005中引入了ROW_NUMBER() 函数,通过这个函数,可以根据给定好的排序字段规则,生成记录序号,其基本用法为:

SELECT  ROW_NUMBER() OVER ( ORDER BY dbo.Products.ProductID DESC ) AS rownum ,
        *
FROM    dbo.Products

这样,结果集中第一列就为 rownum,从1开始按步长为1递增,这有点类似从1开始步长为1的自增字段。 这里需要提一下的是,这个语句中赋值的rownum列不能使用在当前的where语句中,也不可以把整个ROW_NUMBER()语句放到where中作为条件,下面两种使用方式都是错误的。

SELECT  ROW_NUMBER() OVER ( ORDER BY dbo.Products.ProductID DESC ) AS rownum ,
        *
FROM    dbo.Products
WHERE rownum BETWEEN 1 AND 10

会提示错误:

Invalid column name 'rownum'.
SELECT  ROW_NUMBER() OVER ( ORDER BY dbo.Products.ProductID DESC ) AS rownum ,
        *
FROM    dbo.Products
WHERE ( ROW_NUMBER() OVER (ORDER BY City) AS rown ) BETWEEN 1 AND 10

会提示错误:

Incorrect syntax near the keyword 'AS'.

正确的做法是,把查询的结果作为一个内查询,再在外面套上一个外查询语句:

SELECT  *
FROM    ( SELECT    ROW_NUMBER() OVER ( ORDER BY dbo.Products.ProductID DESC ) AS rownum ,
                    *
          FROM      dbo.Products
        ) AS temp
WHERE   temp.rownum BETWEEN 1 AND 10

有了以上基础之后,我们就可以利用ROW_NUMBER这个特性来进行排序了。

SELECT  *
FROM    ( SELECT TOP ( @pageSize * @pageIndex )
                    ROW_NUMBER() OVER ( ORDER BY dbo.Products.UnitPrice DESC ) AS rownum ,
                    *
          FROM      dbo.Products
        ) AS temp
WHERE   temp.rownum > ( @pageSize * ( @pageIndex - 1 ) )
ORDER BY temp.UnitPrice

策略很简单,首先我们选取包含要查页的数据,然后使用ROW_NUMER函数进行编号, 然后在外查询中指定rownum大于页起始记录即可。这种方式简单快捷。

这里还有一种使用CTE的方式 (common_table_expression,公用表表达式,不是CTE四六级哦, 我第一次接触到这个是面试的时候被问到如何使用SQL编写递归, 呵呵),使用很简单,就是把内查询放在CTE 里面,如下:

WITH    ProductEntity
          AS ( SELECT TOP ( @pageSize * @pageIndex )
                        ROW_NUMBER() OVER ( ORDER BY dbo.Products.UnitPrice DESC ) AS rownum ,
                        *
               FROM     dbo.Products
             )
SELECT  *
FROM    ProductEntity
WHERE   ProductEntity.rownum > ( @pageSize * ( @pageIndex - 1 ) )
ORDER BY ProductEntity.UnitPrice

这种性能和上面的类似。但是在某些情况下, 使用CTE会比直接采用外接查询具有更好的效率。例如,我们可以仅使用CTE来存储行号,关键字以及排序字段,然后用来和原表做join查询,如下:

WITH    ProductEntity
          AS ( SELECT TOP ( @pageSize * @pageIndex )
                        ROW_NUMBER() OVER ( ORDER BY dbo.Products.UnitPrice DESC ) AS rownum ,
                        ProductID ,--主键,
                        UnitPrice--待排序字段
               FROM     dbo.Products
             )
SELECT  *
FROM    ProductEntity
        INNER JOIN dbo.Products ON dbo.Products.ProductID = ProductEntity.ProductID
WHERE   ProductEntity.rownum > ( @pageSize * ( @pageIndex - 1 ) )
ORDER BY ProductEntity.UnitPrice

使用ROW_NUMBER来进行分页是一种使用很广的分页方式, 在本文开头讲到在LINQ中可以采用的TAKE 和 SKIP语句,但是与数据库交互只能使用SQL语句,LINQ在内部会帮我们转化为合适的SQL语句,语句里面其实也是采用ROW_NUMBER这一函数,为了演示,我们新建一个Console程序,然后在里面添加一个LINQ To SQL的类,使用方法非常简单,如下:

List<Product> product;
int pageSize = 10;
int pageIndex = 8;
using (ProductsDataContext context = new ProductsDataContext())
{
    product = context.Products.OrderByDescending(x => x.UnitPrice)//排序
                                .Skip(pageSize * (pageIndex-1))//跳过前面的记录
                                .Take(pageSize)//选取每一页个数
                                .ToList();
}

寥寥几句就实现了分页。

我们知道LINQ其实是将C#表达式树转换成了SQL语言,通过SQLServer Profile 工具,我们可以看到程序发送给SQLServer的请求,如下:

我把下面的语句拷贝出来,可以看到

EXEC sp_executesql N'SELECT [t1].[ProductID], [t1].[ProductName], [t1].[SupplierID], [t1].[CategoryID], [t1].[QuantityPerUnit], [t1].[UnitPrice], [t1].[UnitsInStock], [t1].[UnitsOnOrder], [t1].[ReorderLevel], [t1].[Discontinued]
FROM (
    SELECT ROW_NUMBER() OVER (ORDER BY [t0].[UnitPrice] DESC) AS [ROW_NUMBER], [t0].[ProductID], [t0].[ProductName], [t0].[SupplierID], [t0].[CategoryID], [t0].[QuantityPerUnit], [t0].[UnitPrice], [t0].[UnitsInStock], [t0].[UnitsOnOrder], [t0].[ReorderLevel], [t0].[Discontinued]
    FROM [dbo].[Products] AS [t0]
    ) AS [t1]
WHERE [t1].[ROW_NUMBER] BETWEEN @p0 + 1 AND @p0 + @p1
ORDER BY [t1].[ROW_NUMBER]', N'@p0 int,@p1 int', @p0 = 70, @p1 = 10

这正是我们之前手写的采用ROW_NUMBER 的分页程序。可见,简简单单的一句SKIP和TAKE,LINQ在后面帮我们做了很多工作。

使用OFFSET FETCH子句分页

既然LINQ这么简单的搞定了分页,那么SQLServer中有没有类似的简单的语句就能搞定分页了,答案是有的,那就是SQL Server Compact 4.0中引入的OFFSET FETCH子句。

SELECT  *
FROM    dbo.Products
ORDER   BY UnitPrice DESC
OFFSET  ( @pageSize * ( @pageIndex - 1 )) ROWS
FETCH NEXT @pageSize ROWS ONLY;

是不是和LINQ很像,OFFSEET相当于SKIP,FETCH NEXT相当于TAKE。

可以在官网上下载SQL Server CE 4.0,目前仅支持SQL Server 2012及SQL Server 2014,不过可以使用Microsoft Webmatrix这个工具来用这一新功能。

比较

在讨论性能之前,首先需要明确的是,我们在编写SQL语句的时候,尽量要减少不必要字段的输出,文中出于演示,所以都用的*,在实际中不要这样。还有就是要根据业务逻辑,比如查询条件,建立合适的聚合索引和非聚合索引,索引对于查找的效率影响非常大,SQL中的索引其实就是建立某种平衡查找树,如B树来进行,这方面的知识可以看我之前写的算法中的文章,再有就是了解一下SQL Server 的一些特性比如CTE,IN 和Exist的区别等等,有些小的地方对性能可能有一定的影响。

在上面这些处理好了之后,我们现在来讨论那种分页方案更好。

以上是对SQLServer数据库SQL分页的一点总结,希望对您有所帮助。

浅谈SQL Server数据库分页的更多相关文章

  1. c#Winform程序调用app.config文件配置数据库连接字符串 SQL Server文章目录 浅谈SQL Server中统计对于查询的影响 有关索引的DMV SQL Server中的执行引擎入门 【译】表变量和临时表的比较 对于表列数据类型选择的一点思考 SQL Server复制入门(一)----复制简介 操作系统中的进程与线程

    c#Winform程序调用app.config文件配置数据库连接字符串 你新建winform项目的时候,会有一个app.config的配置文件,写在里面的<connectionStrings n ...

  2. 【SqlServer系列】浅谈SQL Server事务与锁(上篇)

    一  概述 在数据库方面,对于非DBA的程序员来说,事务与锁是一大难点,针对该难点,本篇文章视图采用图文的方式来与大家一起探讨. “浅谈SQL Server 事务与锁”这个专题共分两篇,上篇主讲事务及 ...

  3. 浅谈SQL Server内部运行机制

    对于已经很熟悉T-SQL的读者,或者对于较专业的DBA来说,逻辑的增删改查,或者较复杂的SQL语句,都是非常简单的,不存在任何挑战,不值得一提,那么,SQL的哪些方面是他们的挑战 或者软肋呢? 那就是 ...

  4. 浅谈SQL Server数据内部表现形式

    在上篇文章 浅谈SQL Server内部运行机制 中,与大家分享了SQL Server内部运行机制,通过上次的分享,相信大家已经能解决如下几个问题: 1.SQL Server 体系结构由哪几部分组成? ...

  5. 浅谈SQL Server事务与锁(上篇)

    一  概述 在数据库方面,对于非DBA的程序员来说,事务与锁是一大难点,针对该难点,本篇文章试图采用图文的方式来与大家一起探讨. “浅谈SQL Server 事务与锁”这个专题共分两篇,上篇主讲事务及 ...

  6. 浅谈SQL Server中的事务日志(一)----事务日志的物理和逻辑构架

    简介 SQL Server中的事务日志无疑是SQL Server中最重要的部分之一.因为SQL SERVER利用事务日志来确保持久性(Durability)和事务回滚(Rollback).从而还部分确 ...

  7. 浅谈SQL Server中的快照

    原文地址:http://www.cnblogs.com/CareySon/archive/2012/03/30/2424880.html 简介 数据库快照,正如其名称所示那样,是数据库在某一时间点的视 ...

  8. 浅谈SQL Server 对于内存的管理

    简介 理解SQL Server对于内存的管理是对于SQL Server问题处理和性能调优的基本,本篇文章讲述SQL Server对于内存管理的内存原理. 二级存储(secondary storage) ...

  9. (转)浅谈SQL Server 对于内存的管理

    简介 理解SQL Server对于内存的管理是对于SQL Server问题处理和性能调优的基本,本篇文章讲述SQL Server对于内存管理的内存原理. 二级存储(secondary storage) ...

随机推荐

  1. Android 常用代码

    1.单元测试 然而可以直接建立单元测试 <uses-library android:name="android.test.runner"/> 放在application ...

  2. 基于Codeigniter框架实现的APNS批量推送—叮咚,查水表

    最近兼职公司已经众筹成功的无线门铃的消息推送出现了问题,导致有些用户接收不到推送的消息,真是吓死宝宝了,毕竟自己一手包办的后台服务,影响公司信誉是多么的尴尬,容我简单介绍一下我们的需求:公司开发的是一 ...

  3. ubuntu_nfs搭建

    搭建步骤: 1.sudo apt-get install nfs-kernel-server 2.执行命令:mkdir /home/wmx/Desktop/nfs 搭建一个nfs服务专有的文件夹,这里 ...

  4. Windows远程桌面打印机映射

    计算机的打印机驱动能打印,需要满足两个条件,一个是有打印驱动本身,一个是要有连接好了的端口.这样,打印作业就会被打印驱动程序封装成一种打印机能识别的组织形式,然后通过打印端口发送给打印机,然后打印! ...

  5. C++结构体内存对齐跨平台测试

    测试1,不规则对齐数据. Code: #include <stdio.h> #pragma pack(push) #pragma pack(8) struct Test8 { char a ...

  6. Arduino uno 教程~持续更新~

    http://arduino.osall.com/index.html http://study.163.com/search.htm?t=2&p=Arduino http://www.ard ...

  7. Git - 问题集

    1.If no other git process is currently running, this probably means a git process crashed in this re ...

  8. Nginx - Windows下nginx定时分割日志

    1.建立批处理脚本,c:\soft\demo.bat @echo off taskkill /F /IM nginx.exe > nul cd C:\soft\nginx-1.11.3 rem ...

  9. php sprintf 函数的用法

    sprintf() 函数把格式化的字符串写入变量中. arg1.arg2.++ 参数将被插入到主字符串中的百分号(%)符号处.该函数是逐步执行的.在第一个 % 符号处,插入 arg1,在第二个 % 符 ...

  10. Node.js在Chrome进行调试

    在开发node.js环境时候,调试是一件很疼苦的事情,不过随着时代不断发展,先如今已经有很多种node环境代码调试方式,今天我就笔记一下我使用的方式 node-inspector: node-insp ...