SQL Server 2012+ 推荐用 OFFSET/FETCH 分页,须配合 ORDER BY 和索引;@skip 是偏移行数非页码;深度分页(>10万)性能差;EF Core 需原生 SQL,防注入用 ExecuteSqlInterpolated;前端参数须校验范围并防溢出;总数统计建议缓存或加 NOLOCK 优化。

SQL Server 用 OFFSET + FETCH NEXT 最直接
SQL Server 2012+ 原生支持标准分页语法,比老式 ROW_NUMBER() 嵌套查询更简洁、可读性更好,也更容易让查询优化器生成合理执行计划。
常见错误是把 OFFSET 写成变量但没加 ORDER BY —— 这会直接报错:The OFFSET clause cannot be used without an ORDER BY clause.
-
OFFSET @skip ROWS FETCH NEXT @take ROWS ONLY是必须搭配ORDER BY使用的,且排序字段最好有索引(比如主键或时间戳) -
@skip是跳过的行数,不是页码;第 3 页每页 20 条,要传40,别传3 - 注意
OFFSET越大性能越差(尤其 >10 万行后),不是所有场景都适合深度分页
.NET Core 中用 FromSqlRaw 或 ExecuteSqlInterpolated 传参
Entity Framework Core 不支持直接在 LINQ 中写 OFFSET/FETCH,得走原生 SQL。用 ExecuteSqlInterpolated 更安全(自动参数化,防注入),比拼接字符串强得多。
别用 string.Format 或 $"" 拼 SQL —— 一旦用户输入带单引号的关键词,立刻 SQL 注入。
立即学习“前端免费学习笔记(深入)”;
- 查总数仍需单独
SELECT COUNT(*),EF Core 的CountAsync()不能和FromSqlRaw分页结果共用同一查询 - 返回实体时,确保 SQL 查询字段顺序、名称和目标类型属性完全一致,否则映射为空
- 如果用 Dapper,直接
connection.QueryAsync<T>(sql, new { skip = (page-1)*size, take = size })更轻量
前端传参别只信 page 和 pageSize
很多前端组件(如 Element Plus、Ant Design Vue)默认传 current 和 pageSize,后端别硬编码解析为 page 和 size 就完事——得校验范围。
典型翻车点:用户手动改 URL 里的 page=9999999,后端不拦着,数据库直接卡死。
- 限制
page≤ 1000,pageSize≤ 100(业务允许的话设更小,比如 20 或 50) - 把
skip = (page - 1) * pageSize转成long类型再算,避免 int 溢出(比如 page=100000, size=1000 → 99999000000) - 返回 JSON 里除了数据列表,至少带上
total(总条数)、page、pageSize,前端分页控件依赖这些
总数统计和分页查询别放在一个事务里硬扛
高并发下,COUNT(*) 和分页主查询共用同一张大表,容易锁表或拖慢响应。尤其当 WHERE 条件复杂、涉及多表 JOIN 时,COUNT 往往比主查询还慢。
有些团队用缓存总数(比如 Redis 存个近似值),但要注意:如果业务对“精确总页数”不敏感(比如日志列表、通知列表),可以接受误差,那就没必要每次查真实总数。
- 对实时性要求高的场景(如订单管理),COUNT 必须走,但建议给 COUNT 查询加
NOLOCK(SQL Server)或等价 hint,降低阻塞风险 - 如果表超千万行且 WHERE 条件固定,考虑建覆盖索引,把 COUNT 所需字段全包含进去
- 别在同一个 EF Core
DbContext实例里连续调用CountAsync()和FromSqlRaw().ToListAsync()—— 避免上下文状态混乱
实际分页逻辑最易被忽略的,是深度偏移(OFFSET 很大)时的性能断崖和前端未校验参数导致的后端资源耗尽。这两点不提前压测、不加防护,上线后第一个流量高峰就可能拖垮数据库。


















