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

SQL Server分页怎么搞才快又稳,聊聊那些实用的分页技巧和代码实现

说到SQL Server里分页,这绝对是每个开发人员都绕不开的话题,尤其是当数据量上了百万、千万级别,一个没写好的分页查询,可能就直接让数据库服务器“罢工”了,怎么才能又快又稳地搞定分页呢?我们来聊聊那些真正实用的技巧和代码。

老办法的痛点:ROW_NUMBER() OVER()

最常见的分页写法,大概是这样的,来自各种网络教程和早期项目实践:

SELECT * FROM (
    SELECT ROW_NUMBER() OVER (ORDER BY CreateTime DESC) AS RowNum, *
    FROM YourBigTable
) AS T
WHERE T.RowNum BETWEEN 1000001 AND 1000100;

这个方法逻辑清晰,很容易理解,就是先给整个结果集按排序规则生成一个连续的行号,然后根据指定的页码和大小,截取中间那一段。

它的“坑”也很明显BETWEEN 1000001 AND 1000100 这个操作,意味着数据库必须先为前100万条数据生成行号,然后才能拿到你想要的那100条,你翻页越深,需要“抛弃”的临时数据就越多,效率自然就越低,如果你的表有几千万行,翻到最后一页,代价会非常高昂。

提速的关键:使用“锚点”思路(Keyset Pagination)

为了解决深度分页变慢的问题,一个更聪明的办法是“记住上一页最后一条记录的位置”,然后直接从它后面开始取,这种方法常被称为“Keyset Pagination”或“Seek Method”,在《SQL权威指南》等经典书籍中都有提及。

它的核心思想是:不让数据库去计算庞大的行号,而是利用索引(通常是主键或排序列)进行快速定位。

假设我们有一个文章表 Articles,主键是 Id,我们按发布时间 CreateTime 降序排列。

传统分页(慢)

-- 获取第10001-10010条记录(慢)
SELECT * FROM (
    SELECT ROW_NUMBER() OVER (ORDER BY CreateTime DESC, Id DESC) AS RowNum, *
    FROM Articles
) AS T
WHERE T.RowNum BETWEEN 10001 AND 10010;

“锚点”分页(快)

-- 假设我们已知上一页最后一条记录的 CreateTime 和 Id
DECLARE @LastSeenCreateTime DATETIME = '2023-10-01 12:00:00';
DECLARE @LastSeenId INT = 12345;
-- 直接取“锚点”之后的10条
SELECT TOP 10 *
FROM Articles
WHERE
    (CreateTime < @LastSeenCreateTime)
    OR (CreateTime = @LastSeenCreateTime AND Id < @LastSeenId)
ORDER BY CreateTime DESC, Id DESC;

为什么这个方法快?

  1. 利用了索引CreateTimeId 上有合适的索引,这个查询会变成一个非常高效的索引查找(Index Seek),直接跳到数据所在的位置,而不是扫描整个表。
  2. 避免了计算行号:数据库不需要知道当前是第多少页,它只关心“从哪个点开始往后拿数据”,数据量的大小不影响查询速度。

实际应用中的细节

  1. 排序稳定性:为了确保分页稳定(即翻页时数据不重复、不丢失),ORDER BY 子句必须能唯一确定一行,通常的做法是在主排序列后面加上唯一列(如主键 Id)。ORDER BY CreateTime DESC, Id DESC

  2. 前端配合:使用“锚点”分页,前端不能像传统分页那样直接跳转到任意页码(比如从第1页直接跳到第100页),因为它需要知道上一页最后一条记录的信息,这种模式更适合“无限滚动”或“上一页/下一页”这种顺序浏览的场景,如果需要跳页,可以稍微变通一下,比如先快速查询出目标页的起始“锚点”。

  3. OFFSET FETCH 语法(SQL Server 2012+) 从SQL Server 2012开始,引入了更标准的 OFFSET ... FETCH 语法,让分页查询写起来更简洁:

    SELECT *
    FROM Articles
    ORDER BY CreateTime DESC, Id DESC
    OFFSET 10000 ROWS
    FETCH NEXT 10 ROWS ONLY;

    但要注意:这个语法只是 ROW_NUMBER() 方法的语法糖,底层原理是一样的,所以它同样存在深度分页性能下降的问题,它适用于数据量不大或翻页不深的场景,写起来方便,对于海量数据深度分页,它并不是性能最优解。

总结与选择

  • 小数据量或浅分页:用 OFFSET FETCHROW_NUMBER(),代码简洁明了。
  • 大数据量或深度分页强烈推荐“锚点”分页法,这是保证性能的关键,虽然对前端交互有一定要求,但为了系统的稳定性,这个代价是值得的。
  • 索引是基础:无论用哪种方法,一个针对排序列设计良好的索引是分页查询快的根本前提,没有索引,任何技巧都是空中楼阁。

性能优化没有银弹,最好的方法是根据你的实际数据量、用户访问习惯(是顺序浏览多还是随机跳页多)来选择和组合这些技巧,必要时通过执行计划来分析和验证。

SQL Server分页怎么搞才快又稳,聊聊那些实用的分页技巧和代码实现