游标分页比OFFSET快,因其基于排序键范围查询、可走索引下推,而OFFSET需全量扫描前N行;C#中OFFSET需限制page_size、校验skip值、用AsNoTracking();游标分页须确保排序字段有索引、非NULL、唯一或复合后备,并注意时区与精度。
游标分页为什么比 OFFSET 快?
因为 offset 每次都要从头扫描跳过前 n 行,数据越靠后,扫描越多,i/o 和 cpu 成倍涨。游标分页只查「上一页最后一条的排序键值之后」的数据,本质是范围查询,能走索引下推。
典型场景:用户刷动态、订单列表、日志流——只要数据有唯一且单调递增/递减的字段(比如 Id、CreatedAt),就该用游标。
- 必须保证排序字段有索引,且不含 NULL(否则游标边界失效)
- 不能混用多个方向排序(如
ORDER BY CreatedAt DESC, Id ASC),游标条件难写还易漏数据 - 如果排序字段可能重复(比如多个记录同秒级
CreatedAt),必须加一个唯一后备字段(如Id)组成复合游标
OFFSET 分页在 C# 中怎么写才不崩?
直接用 Skip() + Take() 是最常见写法,但生产环境必须加防护:
- 限制最大
page_size(建议 ≤ 100),防止用户传Take(10000)拖垮数据库 - 对
skip值做校验,避免负数或超大整数(如int.MaxValue)导致 SQL Server 报错Arithmetic overflow error converting expression to data type int - Entity Framework Core 7+ 支持
AsNoTracking()配合分页,查完即丢,省内存;老版本务必手动关跟踪
示例:
var items = await context.Orders
.Where(x => x.Status == "Shipped")
.OrderByDescending(x => x.Id)
.AsNoTracking()
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
游标分页的 C# 实现要点
核心不是“记住页码”,而是“记住上一页末尾的排序键”。比如按 Id DESC 分页,请求参数应是 cursor=12345(代表上一页最后一条的 Id),下一页查 WHERE Id 。
- 前端传参别用
page=2,改用after=12345或before=67890,语义清晰且防篡改 - C# 后端解析时,用
long.TryParse()而非Convert.ToInt64(),避免空字符串或非法格式抛异常 - 首次请求没有游标,要明确区分逻辑:无
after时查TOP N;有则加WHERE Id - 记得把新一页最后一条的
Id作为下个游标返回给前端,别漏了
EF Core 游标分页怎么写 SQL 才高效?
别依赖 Skip()/Take() 自动生成游标逻辑——它不会帮你转成 WHERE 条件。必须手写查询,或封装成可复用的方法。
- 用原始 SQL 最可控:
SELECT TOP(@size) * FROM Orders WHERE Id
- 用 LINQ 也行,但得显式拼条件:
var query = context.Orders.Where(x => x.Status == "Shipped"); if (cursor.HasValue) query = query.Where(x => x.Id x.Id) .Take(pageSize) .ToListAsync(); - 注意:EF Core 的
Take()在 SQL Server 上生成TOP,但 PostgreSQL 需用LIMIT,跨数据库时建议用 Dapper 或统一抽象层
最容易被忽略的是排序字段的时区和精度问题——比如用 DateTime 当游标,SQL Server 默认精度是 3.3ms,两个不同毫秒级时间可能被存成相同值,导致漏数据。真要用时间字段,至少升到 datetime2(7) 并确保 C# 端用 DateTimeKind.Utc。











