什么是数据库分页?在C#中如何实现高效分页查询?

来源:R语言教程作者:松本一香头衔:网络博主
导读:本期聚焦于松本一香创作的《什么是数据库分页?在C#中如何实现高效分页查询?》,敬请观看详情。当数据表中的记录增长到几十万甚至上千万条时,一次性把全部数据加载到内存显然不现实,分页查询就成了必备手段。数据库分页的核心思路是每次只从数据库中取出当前页需要的那一小部分数据,既降低网络传输量,也减少内存压力。本文先解释OFFSET和FETCH、ROW_NUMBER等分页原理,再通过C#代码演示如何用LINQ的Skip与Take、EF Core以及手写SQL实现分页,并对比各方案在性能上的差异,最后介绍大偏移量场景下的游标分页优化技巧,帮助你写出既正确又高效的生产级分页代码。

分页是几乎所有业务系统都会遇到的需求:订单列表、用户管理、日志查询,只要数据量超过一页能展示的范围,就需要分页。很多初学者以为分页只是前端展示层面的事,实际上真正关键的分页逻辑发生在数据库层。如果在C#中先查询全部数据再用Skip和Take在内存中截取,数据量一大程序就会卡顿甚至崩溃。本文将从原理到实践,系统讲解数据库分页的实现方式与性能优化。

什么是数据库分页?在C#中如何实现高效分页查询?

数据库分页的基本原理

所谓数据库分页,就是客户端每次只请求第N页、每页M条记录,数据库只返回这M条数据,而不是整张表。举例来说,一张有百万条记录的订单表,用户查看第1页时只需返回前20条,数据库端完成筛选和截取,网络传输和内存占用都大幅降低。

在SQL Server 2012及以上版本中,标准写法是使用OFFSET ... FETCH子句,它必须配合ORDER BY使用:

SELECT Id, OrderNo, Amount, CreatedAt
FROM Orders
ORDER BY CreatedAt DESC, Id DESC
OFFSET 0 ROWS FETCH NEXT 20 ROWS ONLY;

其中OFFSET表示跳过的行数,FETCH NEXT表示取多少行。翻到第10页时,OFFSET就是9乘以20等于180。在旧版SQL Server或MySQL中,常用ROW_NUMBER()窗口函数或LIMIT offset, size实现同样的效果。无论哪种写法,都必须有明确且稳定的排序字段,否则同一页在两次查询中可能出现重复或遗漏记录。

在C#中使用LINQ与EF Core实现分页

在Entity Framework Core中,分页写法非常直观:Skip()负责跳过指定数量的记录,Take()负责取指定数量,两者组合后EF Core会自动翻译成数据库端的分页SQL,而不会把整表加载到内存。

public class PagedResult<T>
{
    public List<T> Items { get; set; }
    public int TotalCount { get; set; }
    public int PageIndex { get; set; }
    public int PageSize { get; set; }
    public int TotalPages => (int)Math.Ceiling(TotalCount / (double)PageSize);
}

public async Task<PagedResult<Order>> GetOrdersAsync(int pageIndex, int pageSize)
{
    if (pageIndex < 1) pageIndex = 1;
    if (pageSize < 1 || pageSize > 100) pageSize = 20;

    var query = _dbContext.Orders
        .OrderByDescending(o => o.CreatedAt)
        .ThenByDescending(o => o.Id);

    var totalCount = await query.CountAsync();
    var items = await query
        .Skip((pageIndex - 1) * pageSize)
        .Take(pageSize)
        .ToListAsync();

    return new PagedResult<Order>
    {
        Items = items,
        TotalCount = totalCount,
        PageIndex = pageIndex,
        PageSize = pageSize
    };
}

上面这段代码有几个值得注意的细节。第一,排序字段选择了CreatedAt加Id的组合,Id作为兜底排序可以保证排序的唯一性,避免分页边界处出现数据重复。第二,pageSize做了上限校验,防止恶意请求一次拉取上万条数据拖垮服务。第三,CountAsync与分页查询是两次独立的数据库往返,如果列表页不需要精确的总条数,可以省略统计以提升性能。

大偏移量分页的性能问题与游标分页优化

OFFSET分页有一个隐藏的性能陷阱:偏移量越大,数据库需要扫描并丢弃的行就越多。查询第100万页时,数据库实际要扫过前2000万行再丢弃它们,只为了返回20条结果,响应时间会随页码线性劣化。对于CSDN、淘宝这类深分页场景,这种写法难以承受。

优化思路之一是改用游标分页,也叫键集分页。客户端不再传页码,而是传上一页最后一条记录的排序值,数据库利用索引直接定位起点,扫描量与页码无关:

// 前端传递 lastCreatedAt 和 lastId,而不是 pageIndex
public async Task<List<Order>> GetOrdersByCursorAsync(
    DateTime? lastCreatedAt, long? lastId, int pageSize)
{
    var query = _dbContext.Orders
        .OrderByDescending(o => o.CreatedAt)
        .ThenByDescending(o => o.Id)
        .AsQueryable();

    if (lastCreatedAt.HasValue && lastId.HasValue)
    {
        query = query.Where(o => o.CreatedAt < lastCreatedAt
            || (o.CreatedAt == lastCreatedAt && o.Id < lastId));
    }

    return await query.Take(pageSize).ToListAsync();
}

这种写法完全避开了OFFSET,只要(CreatedAt, Id)上建有复合索引,无论翻到多深的位置,查询耗时都基本恒定。它的代价是失去了随机跳页的能力,用户只能上一页下一页地顺序浏览,但绝大多数用户翻页时根本不会跳到几百页之后,因此游标分页在信息流、消息记录等场景中是更优解。

除了游标分页,还可以采用子查询延迟关联的技巧:先用覆盖索引快速定位目标页的主键集合,再回表取完整字段,能显著减轻深分页时的IO压力。同时要养成用ToQueryString()查看EF Core生成的SQL的习惯,确认分页确实下推到了数据库,而不是发生在内存中,这是验证分页实现是否高效最直接的手段。

数据库分页C#分页查询LINQ分页修改时间:2026-09-01 14:52:36

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。