sql server 2012+ 分页必须用 offset/fetch next 配合 order by(字段需有索引),禁用 row_number() 嵌套;ef core 需原生 sql 实现,注意 sql 注入防护、参数校验、溢出处理及深度分页优化。

SQL Server 2012+ 必须用 OFFSET/FETCH NEXT,别写 ROW_NUMBER() 嵌套查询——后者在数据量大时执行计划容易崩,且可读性差、维护成本高。
OFFSET/FETCH 必须带 ORDER BY,且排序字段要有索引
不加 ORDER BY 直接跑 OFFSET @skip ROWS FETCH NEXT @take ROWS ONLY 会报错:The OFFSET clause cannot be used without an ORDER BY clause.
-
ORDER BY字段建议是主键、时间戳或有唯一索引的列;用无索引字段(比如WHERE Name LIKE '%xxx%'后再ORDER BY Name)会导致全表扫描 + 排序,性能断崖式下跌 - 如果业务必须按非索引字段排序,考虑提前建覆盖索引,或改用搜索+缓存方案,而不是硬扛分页
-
@skip是跳过的行数,不是页码:第 5 页每页 20 条,要传@skip = 80,不是5
EF Core 里不能直接 LINQ 写 OFFSET/FETCH
EF Core 不支持 .Skip().Take() 翻译成 OFFSET/FETCH(除非你用的是非常新的 8.0+ 且显式启用),默认生成的是低效的子查询。必须走原生 SQL。
- 用
ExecuteSqlInterpolated或FromSqlRaw,优先选前者:context.Users.FromSqlInterpolated($"SELECT * FROM Users ORDER BY Id OFFSET {skip} ROWS FETCH NEXT {take} ROWS ONLY") - 绝对不要用
$"SELECT ... {skip} ..."拼字符串——用户输入带单引号或分号就触发 SQL 注入 - 返回实体时,SQL 字段名、顺序、类型必须和目标类完全一致,否则映射结果全为默认值(
null或0) - 总数统计得另起一句
SELECT COUNT(*),EF Core 的CountAsync()无法复用同一个分页查询
前端传参不校验,后端直接卡死
很多前端组件(如 Ant Design Vue 的 a-table)默认传 current=9999999 和 pageSize=1000,后端若不做拦截,skip = (9999999 - 1) * 1000 会溢出 int,或让数据库执行深度分页(>10 万行偏移),CPU 拉满、连接超时。
- 强制校验:
page≤ 1000,pageSize≤ 100(业务允许就设成 20 或 50) -
skip计算前转long:long skip = (long)(page - 1) * pageSize;,避免整数溢出 - 总数查询加
WITH (NOLOCK)(仅限读多写少场景),或把总数缓存 30 秒以上,避免每次翻页都 COUNT - 返回 JSON 至少含
total、page、pageSize,前端分页控件依赖这三个字段驱动 UI
深度分页(OFFSET > 10 万)本质是反模式,真要查第 5000 页,说明 UI 设计有问题——该上搜索、筛选或游标分页(WHERE id > @lastId),而不是靠偏移硬扛。










