当前位置:首页 > 问答 > 正文

用SQLServer咋搞分页查询,数据多了翻页才方便啊

整理自CSDN博客、博客园、知乎等技术社区常见讨论,结合SQLServer官方文档中的基础语法说明)

用SQLServer搞分页查询,确实是个老生常谈但又非常实际的问题,当数据量一大,成千上万条记录,如果一次性全部从数据库里拿出来,前端的页面会卡死,服务器的内存压力也巨大,分页查询的核心思想就是“每次只拿一勺”,而不是“把整个锅端过来”。

在SQLServer中,实现分页查询有几种常见的方法,它们随着SQLServer版本的更新也在不断演进,下面我就按从旧到新、从复杂到简单的顺序,直接跟你说说怎么搞。

最经典(也最啰嗦)的方法:TOP和子查询(适用于早期版本)

在SQLServer 2005及更早的版本里,没有专门的分页命令,大家普遍用一种“土办法”,思路是分两步走:

  • 第一步: 先用TOP关键字取出当前页以及之前所有页的数据,比如你要查第3页,每页10条,那就用TOP 30,取出前30条记录。
  • 第二步: 再把前两页的数据(前20条)从这个结果集中排除掉,剩下的就是第3页的10条了,排除的方法通常是用一个子查询,查询出前(当前页码-1)*每页条数条记录的ID,然后让主查询排除这些ID。

举个例子,假设有张表叫Orders,我们想按OrderID排序,取第3页(每页10行)的数据,SQL写法大概长这样:

-- 假设每页10条,取第3页(即第21到30条)
SELECT TOP 10 * FROM Orders
WHERE OrderID NOT IN (
    SELECT TOP 20 OrderID FROM Orders ORDER BY OrderID
)
ORDER BY OrderID;

(来源:早期SQLServer技术博客和论坛的常见解法)

解读一下:

  • 里面的子查询 SELECT TOP 20 OrderID FROM Orders ORDER BY OrderID 意思是找出按OrderID排序后的前20条订单的ID(也就是第1页和第2页的所有数据)。
  • 外面的主查询 SELECT TOP 10 * ... WHERE OrderID NOT IN (...) 意思是从整个表里,排除掉刚找到的前20条ID,然后从剩下的数据中再取前10条,因为已经排除了前20条,所以这时候取到的“前10条”自然就是原本的第21到30条了。

这种方法虽然能实现效果,但缺点很明显:

  • 写法繁琐: 每次都要写两层查询,排序条件要写两遍,容易出错。
  • 性能问题: 当页码非常靠后时,比如要取第1000页,子查询就要先取出TOP 9990条记录的ID,效率会越来越低。

升级版:ROW_NUMBER()窗口函数(SQLServer 2005及以后版本)

从SQLServer 2005开始,引入了一个强大的功能叫窗口函数,其中ROW_NUMBER()函数专门用来给查询结果的每一行生成一个连续的行号,这简直就是为分页量身定做的。

它的思路更直接:

用SQLServer咋搞分页查询,数据多了翻页才方便啊

  • 先把整个查询结果(或者满足条件的部分)按照你想要的排序规则,给它每一行都标上一个行号(第1行、第2行、第3行...)。
  • 把带有行号的这个结果集当作一个临时表(派生表),直接从里面根据行号范围取数据就行了。

还用上面的例子,查Orders表的第3页(每页10条):

-- 使用ROW_NUMBER()
SELECT * FROM (
    SELECT *,
        ROW_NUMBER() OVER (ORDER BY OrderID) AS RowNum
    FROM Orders
) AS TempTable
WHERE RowNum BETWEEN 21 AND 30;

(来源:SQLServer官方文档关于ROW_NUMBER()函数的介绍及后续版本的最佳实践推荐)

解读一下:

  • ROW_NUMBER() OVER (ORDER BY OrderID) AS RowNum 这一部分是在原始表数据上,按照OrderID排序后,为每一行生成一个叫RowNum的连续序号。
  • 整个 SELECT ..., ROW_NUMBER() ... FROM Orders 被包在一个子查询里,相当于我们创建了一个临时表TempTable,这个表比原表多了一列RowNum
  • 最外层的SELECT * FROM TempTable WHERE RowNum BETWEEN 21 AND 30就非常简单直观了,直接从临时表里筛选出行号在21到30之间的记录。

这种方法比第一种TOP的方法要好得多:

  • 逻辑清晰: 分页的逻辑(BETWEEN)一目了然,不容易写错。
  • 灵活性高: 排序规则只需要在OVER子句里写一次,如果想按其他字段排序,改一个地方就行。
  • 性能尚可: 在排序字段有索引的情况下,效率不错,尤其适合中等数据量的分页。

现代首选:OFFSET-FETCH子句(SQLServer 2012及以后版本)

如果你用的SQLServer是2012版或更新的版本,那么恭喜你,有了一种最简洁、最符合SQL标准的分页方法——OFFSET-FETCH子句,它直接扩展了ORDER BY子句的功能。

语法超级简单:

用SQLServer咋搞分页查询,数据多了翻页才方便啊

ORDER BY 排序列
OFFSET (页码-1)*每页条数 ROWS
FETCH NEXT 每页条数 ROWS ONLY;

还是查第3页(每页10条)的例子:

SELECT * FROM Orders
ORDER BY OrderID
OFFSET 20 ROWS  -- 跳过前20行
FETCH NEXT 10 ROWS ONLY; -- 只取接下来的10行

(来源:SQLServer 2012官方发布说明及新特性文档)

解读一下:

  • OFFSET 20 ROWS:意思很直白,就是跳过前面20行记录,20是怎么来的?(3-1) * 10 = 20。
  • FETCH NEXT 10 ROWS ONLY:跳过之后,从第21行开始,往后取10行。

这是目前最推荐的方法,因为:

  • 语法简洁明了: 意图表达得非常清楚,跳过多少,取多少”。
  • 性能优化: 数据库引擎可以针对这种分页模式进行更好的优化。
  • 符合标准: 这是SQL标准语法,学会它,在其他一些数据库(如PostgreSQL, MySQL 8.0+)中也适用。

实际使用中的一些注意点

不管你用哪种方法,有几个共同的点需要留意:

  • 排序是必须的! 分页一定要有一个确定的排序顺序(ORDER BY),否则数据库每次返回的数据顺序可能不一样,分页就乱套了,最好用唯一性强的列排序(如主键),如果按非唯一列排序,可能出现重复或丢失数据的情况。
  • 性能关键在索引: 分页查询的速度快慢,很大程度上取决于你ORDER BY后面用的那个字段有没有合适的索引,如果没索引,每次分页查询数据库都要对整个表进行排序,数据量一大就慢得不行,给排序字段加索引是优化分页查询最有效的手段。
  • 总页数怎么来? 分页通常还需要知道总数据量,用来计算总页数,这个一般需要用另一个查询单独计算:SELECT COUNT(*) FROM Orders(如果带查询条件,也要加上同样的条件),虽然要执行两次数据库查询,但这是目前通用的做法。

  • 老版本(2005以前):凑合着用TOP和子查询的组合拳。
  • 中等版本(2005及以后):优先使用ROW_NUMBER()函数,逻辑清晰,够用。
  • 新版本(2012及以后):毫不犹豫地用OFFSET-FETCH,这是最爽的方式。

如果你用的SQLServer版本够新,就直接上OFFSET-FETCH吧,代码又好写又容易懂,数据多了翻页才能真正方便起来。