EF Core 的 Skip() + Take() 分页在页码靠后时性能断崖式下降,因数据库需扫描前 N 行;生产环境应优先选用键集分页(基于上页末尾键值)或原生 OFFSET/FETCH 分页,并确保排序字段有唯一索引。

EF Core 的 Skip() + Take() 是最常用、最易理解的分页写法,但它在页码靠后时性能会断崖式下降;真正在生产环境扛住高并发和大数据量的,是键集分页(Keyset Pagination)或原生 SQL 分页(OFFSET/FETCH),而不是无脑链式跳过。
EF Core 中用 Skip() + Take() 分页的坑在哪
这不是语法错误,而是数据库执行逻辑层面的问题:EF Core 把 Skip(10000).Take(20) 翻译成 OFFSET 10000 ROWS FETCH NEXT 20 ROWS ONLY 后,SQL Server 或 PostgreSQL 仍需扫描前 10000 行才能定位起点——索引再好也救不了全表扫描式的偏移。
- 只适合总记录数
- 必须搭配
AsNoTracking(),否则每条实体都进变更追踪器,内存和 GC 压力陡增 - 排序字段没走索引?那
Skip()性能会比不加索引还差——因为排序本身就要临时表 - 不要在
OrderBy里用函数,比如OrderBy(x => x.CreatedAt.Date),这会让索引完全失效
用 OFFSET/FETCH 做原生分页更可控
EF Core 7+ 支持直接翻译 OFFSET/FETCH,但前提是数据库版本达标(SQL Server ≥ 2012、PostgreSQL ≥ 8.4、MySQL ≥ 8.0)。它比 Skip()/Take() 少一层 LINQ 解析开销,也更容易和 Count(*) 拆开优化。
- 总数查询必须单独执行:
SELECT COUNT(*) FROM Users WHERE IsActive = 1,EF Core 的CountAsync()无法复用带OFFSET的查询 - 字段映射要严格一致:SQL 返回列名、顺序、类型必须和目标实体属性完全匹配,否则
FromSqlRaw映射结果全为 null - 参数必须用
ExecuteSqlInterpolated,别用$""拼接——用户搜O'Reilly就直接触发 SQL 注入 - 示例写法:
context.Users.FromSqlInterpolated($"SELECT * FROM Users WHERE IsActive = {isActive} ORDER BY Id OFFSET {skip} ROWS FETCH NEXT {take} ROWS ONLY")
键集分页(Keyset Pagination)才是大数据量的正解
它不依赖页码,而是用上一页最后一条记录的主键(或排序字段组合)作为下一页的起始条件,彻底避开 OFFSET 的扫描代价。典型场景是无限滚动或“下一页”按钮,不能跳转任意页码。
- 核心条件是
WHERE Id > @lastId ORDER BY Id LIMIT 20,数据库能直接走索引定位,毫秒级响应 - 必须确保排序字段有唯一性约束,或组合成唯一键(如
ORDER BY CreatedAt DESC, Id DESC) - 对应索引必须把排序字段放最前面,例如
CREATE INDEX IX_Users_CreatedAt_Id ON Users(CreatedAt DESC, Id DESC) - 前端需传回上一页末尾的
CreatedAt和Id,后端拼进 WHERE 条件,不能只传页码 - 不支持跳转第 N 页,也不适合需要“跳转到末页”的管理后台——这是它的天然限制
动态过滤 + 排序 + 分页怎么统一封装
硬编码每个接口的 Where、OrderBy、Skip 会导致维护爆炸。推荐用 QueryParameters 类统一封装分页参数,并通过扩展方法注入 IQueryable 链。
-
SortBy字段格式应为"Name asc, CreatedAt desc",解析后调用OrderBy/ThenBy动态构建排序 -
Filters是List<FilterItem>,每个含Field、Operator(eq/ne/gt/like)、Value,用 Expression Tree 构建动态Where - 所有扩展方法开头加
.AsNoTracking(),避免无意中开启追踪 - 返回类型用
PagedResponse<T>包装,含Data、TotalRecords、HasNextPage等计算属性,前端不用自己算总页数
键集分页和原生 SQL 分页不是“高级技巧”,而是数据量过万后绕不开的路径;而 Skip()/Take() 看似简单,一旦上线就可能成为慢查询根因——尤其当排序字段没索引、或前端允许传任意大页码时。


















