offset-fetch在第10000页变慢是因为sql server必须扫描并跳过前offset行,io和cpu开销陡增;排序字段重复、无索引或含null时会进一步导致性能下降和结果错乱。

为什么 OFFSET-FETCH 在第 10000 页会变慢
因为 SQL Server 必须扫描并跳过前 OFFSET 行,哪怕你只取 20 条。当 OFFSET 达到几十万时,IO 和 CPU 开销陡增,查询响应从毫秒级变成秒级甚至超时。
这不是 C# 层能优化的——EF Core 的 Skip() 翻译成 OFFSET 后,瓶颈就在数据库执行计划里。你改 LINQ 写法、加索引、调缓存都救不了它。
- 排序字段重复时,
OFFSET分页结果可能错乱(比如同时间创建的多条订单,翻页时漏掉或重复) -
ORDER BY字段没索引?执行计划会走全表扫描,OFFSET越大越卡 - 用
DateTime排序但允许NULL?数据库无法使用索引下推,直接退化为慢查询
游标分页必须满足的三个硬条件
游标分页不是“换种写法”,而是换了一套数据契约。不满足以下任一条件,就别上生产:
- 排序字段必须
NOT NULL且有索引(推荐组合唯一索引,如(CreatedAt DESC, Id DESC)) - 客户端必须保存上一页最后一条记录的排序值(例如
lastCreatedAt和lastId),不能靠页码推算 - 查询语句必须用
WHERE+ 参数化比较,而不是Skip();Take(n + 1)多取 1 条用于判断是否有下一页
示例中漏掉 Id 比较会导致同时间戳订单翻页错乱:.Where(o => o.CreatedAt
EF Core 游标分页怎么写才不掉坑
关键不是“能不能写”,而是“怎么确保它真在数据库执行”。常见失效场景:
- 提前调用了
AsEnumerable()或ToList()—— 整个数据集先拉到内存,再Where,完全失去意义 - 用字符串拼接构造条件(如
$"o.CreatedAt )—— 参数未参数化,SQL 注入风险 + 类型转换失败 - 排序字段类型不一致(比如 C# 用
DateTimeOffset,数据库是DATETIME2)—— EF 可能放弃索引下推
正确姿势是全程保持 IQueryable<order></order> 链式调用:context.Orders.Where(...).OrderByDescending(...).ThenByDescending(...).Take(21)
游标分页的边界情况怎么处理
真实业务里最常被忽略的不是“怎么查下一页”,而是“怎么查第一页”和“怎么处理删除导致的空洞”:
- 第一页没有“上一页最后一条”,应传入最大可能值(如
DateTime.MaxValue)作为游标起点 - 用户刷新页面或跳转链接时,游标值丢失 → 必须降级回
OFFSET分页(仅限首屏),或强制重定向到第一页 - 中间数据被删,下一页查询可能返回少于
pageSize条 —— 这是正常现象,前端要能容错,不能假定每次都有满页
游标值本质是“位置快照”,不是“页码编号”。它快、稳、可扩展,但也意味着你放弃了随机跳页的能力。











