浅谈SQL Server数据库分页

数据库分页是老生常谈的问题了。如果使用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数据库分页

时间: 2024-08-06 20:07:14

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

浅谈SQL Server中的事务日志(四)----在完整恢复模式下日志的角色

浅谈SQL Server中的事务日志(四)----在完整恢复模式下日志的角色 本篇文章是系列文章中的第四篇,也是最后一篇,本篇文章需要前三篇的文章知识作为基础,前三篇的文章地址如下: 浅谈SQL Server中的事务日志(一)----事务日志的物理和逻辑构架 浅谈SQL Server中的事务日志(二)----事务日志在修改数据时的角色 浅谈SQL Server中的事务日志(三)----在简单恢复模式下日志的角色 简介 生产环境下的数据是如果可以写在资产负债表上的话,我想这个资产所占的数额一定不会

浅谈SQL Server中的事务日志(二)----事务日志在修改数据时的角色

浅谈SQL Server中的事务日志(二)----事务日志在修改数据时的角色 本篇文章是系列文章中的第二篇,以防你还没有看过第一篇.上一篇的文章地址如下: 浅谈SQL Server中的事务日志(一)----事务日志的物理和逻辑构架 简介 每一个SQL Server的数据库都会按照其修改数据(insert,update,delete)的顺序将对应的日志记录到日志文件.SQL Server使用了Write-Ahead logging技术来保证了事务日志的原子性和持久性.而这项技术不仅仅保证了ACID

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

浅谈SQL Server中的事务日志(一)----事务日志的物理和逻辑构架 简介 SQL Server中的事务日志无疑是SQL Server中最重要的部分之一.因为SQL SERVER利用事务日志来确保持久性(Durability)和事务回滚(Rollback).从而还部分确保了事务的ACID属性.在SQL Server崩溃时,DBA还可以通过事务日志将数据恢复到指定的时间点.当SQL Server运转良好时,多了解一些事务日志的原理和概念显得并不是那么重要.但是,一旦SQL SERVER发生崩

浅谈SQL Server中的事务日志(三)----在简单恢复模式下日志的角色

浅谈SQL Server中的事务日志(三)----在简单恢复模式下日志的角色 本篇文章是系列文章中的第三篇,前两篇的地址如下: 浅谈SQL Server中的事务日志(一)----事务日志的物理和逻辑构架 浅谈SQL Server中的事务日志(二)----事务日志在修改数据时的角色 简介 在简单恢复模式下,日志文件的作用仅仅是保证了SQL Server事务的ACID属性.并不承担具体的恢复数据的角色.正如”简单”这个词的字面意思一样,数据的备份和恢复仅仅是依赖于手动备份和恢复.在开始文章之前,首先

浅谈SQL Server任务调度

在前面两篇文章中( 浅谈SQL Server内部运行机制 and 浅谈SQL Server数据内部表现形式 ),我们交流了一些关于SQL Server的一些术语,SQL Sever引擎 与SSMS抽象模型,SQL Server内部存储机制和SQL Server内部体系结构等.讨论的这些问题,均可以归为一个问题,即"SQL Server是怎么执行客户端输入的SQL 语句的?”,其中,重点讨论了下图(SQL Server 体系结构) 然而,如果我们仅仅了解如上技术,是不具备一个资深DBA或数据库优化

浅谈SQL Server中的快照

原文地址:http://www.cnblogs.com/CareySon/archive/2012/03/30/2424880.html 简介 数据库快照,正如其名称所示那样,是数据库在某一时间点的视图.是SQL Server在2005之后的版本引入的特性.快照的应用场景比较多,但快照设计最开始的目的是为了报表服务.比如我需要出2011的资产负债表,这需要数据保持在2011年12月31日零点时的状态,则利用快照可以实现这一点.快照还可以和镜像结合来达到读写分离的目的.下面我们来看什么是快照. 什

浅谈SQL Server逻辑读、物理读和预读

[摘要] 对SQL Server数据库进行性能优化时,有一个重要的工作是优化IO开销.通过开启Statistics IO,我们可以了解到语句执行的IO开销,包含物理读.逻辑读.预读等.而我们是否了解这三者的区别和联系呢,相信很多人都不是很理解,下面我们一起来探索这三者究竟分别代表着什么. [正文] 一  .SQLServer数据存储方式 SQL Server的数据库包括数据文件和日志文件,一个数据库可以有一个或多个数据文件或日志文件. 所有的数据都存储在数据文件中,而数据文件可以划分为再小的单元

浅谈SQL Server 对于内存的管理

简介 理解SQL Server对于内存的管理是对于SQL Server问题处理和性能调优的基本,本篇文章讲述SQL Server对于内存管理的内存原理. 二级存储(secondary storage) 对于计算机来说,存储体系是分层级的.离CPU越近的地方速度愉快,但容量越小(如图1所示).比如:传统的计算机存储体系结构离CPU由近到远依次是:CPU内的寄存器,一级缓存,二级缓存,内存,硬盘.但同时离CPU越远的存储系统都会比之前的存储系统大一个数量级.比如硬盘通常要比同时代的内存大一个数量级.

浅谈SQL Server中的三种物理连接操作

简介 在SQL Server中,我们所常见的表与表之间的Inner Join,Outer Join都会被执行引擎根据所选的列,数据上是否有索引,所选数据的选择性转化为Loop Join,Merge Join,Hash Join这三种物理连接中的一种.理解这三种物理连接是理解在表连接时解决性能问题的基础,下面我来对这三种连接的原理,适用场景进行描述. 嵌套循环连接(Nested Loop Join) 循环嵌套连接是最基本的连接,正如其名所示那样,需要进行循环嵌套,这种连接方式的过程可以简单的用下图