SQL Server 2012+分页必须用OFFSET-FETCH,但需额外执行独立COUNT()查询获取总条数,且WHERE条件必须完全一致;不可在分页语句中嵌套COUNT() OVER()或用CTE合并,否则性能严重下降。

SQL Server 2012+ 必须用 OFFSET-FETCH,但 COUNT(*) 得单独查
OFFSET-FETCH 是 SQL Server 唯一原生支持的分页语法,但它不提供总条数——SELECT ... ORDER BY id OFFSET @skip ROWS FETCH NEXT @take ROWS ONLY 这条语句本身无法附带总数。强行在同一个 SELECT 里加 COUNT(*) OVER() 虽然语法合法,但会让每行都重复计算总数,数据量大时性能断崖式下降;更糟的是,它还会让执行计划无法利用索引跳过非必要扫描。
正确做法是拆成两个独立查询:
- 第一步:用
SELECT COUNT(*) FROM table WHERE ...获取@total_count - 第二步:用
SELECT ... OFFSET ... FETCH拉取当前页数据 - 两次查询的
WHERE条件字符串必须完全一致(包括空格、大小写、参数化方式),否则总数和分页结果对不上 - 建议把 COUNT 查询放在前面,并用
SET STATISTICS IO OFF关闭统计输出,避免干扰结果集
为什么别用 ROW_NUMBER() + CTE 包总数?
常见误区是写一个 CTE,里面一边算 COUNT(*),一边用 ROW_NUMBER() 分页,再 JOIN 或 CROSS APPLY 把总数“塞”进结果集。这种写法看似“只查一次”,实际代价更高:
- CTE 中的
COUNT(*)子查询仍需全表扫描(除非有覆盖索引) - 外层
ROW_NUMBER()仍要对全部匹配行排序并编号,内存和 CPU 开销没减少 - SQL Server 优化器往往无法将两个逻辑合并为高效计划,反而放弃并行执行
- 当
WHERE条件复杂或涉及函数时,CTE 内部的估算偏差会放大,导致执行计划失真
存储过程里怎么安全传参并防注入?
动态拼接表名、字段、WHERE 条件时,sp_executesql 是唯一可信赖的方式;直接拼字符串 + EXEC() 极易被注入,且无法复用执行计划。
关键约束:
-
@TableName和@OrderBy必须白名单校验(如查sys.tables确认存在),不能直接拼进 SQL -
@Where参数若含用户输入,必须转为参数化子句,例如:N' AND status = @status',再通过sp_executesql的参数字典传入值 -
COUNT(*)查询也得走同一套参数化逻辑,否则 WHERE 不一致的风险立刻出现 - 不要用
QUOTENAME()处理@OrderBy后直接拼接——它只能防标识符注入,不能解决排序方向(ASC/DESC)被篡改的问题
MySQL / PostgreSQL 用户注意:SQL_CALC_FOUND_ROWS 已废,FOUND_ROWS() 不可靠
MySQL 8.0.17 起彻底移除了 SQL_CALC_FOUND_ROWS;PostgreSQL 从不支持类似机制。现在所有主流数据库都回归“两步法”:
- 先执行带
LIMIT/OFFSET的分页查询 - 紧接着在同一连接、同一事务中执行同条件的
SELECT COUNT(*) - 并发场景下,如果业务允许“总数轻微滞后”,可在 COUNT 查询加
WITH (NOLOCK)(SQL Server)或READ UNCOMMITTED隔离级别,但必须评估脏读风险 - 别用
UNION ALL把分页结果和 COUNT 拼一起——列数、类型不匹配,SQL 直接报错
真正影响性能的从来不是多一次网络往返,而是两次查询是否共享相同执行路径。WHERE 条件稍有差异、索引缺失、或统计信息过期,就会让 COUNT 走聚集扫描而分页走索引查找——这才是总条数不准和慢的根本原因。


















