row_number()分页慢主因是order by字段无匹配索引,导致全表扫描与内存排序;需创建与over子句完全一致的复合索引,并注意where条件、避免表达式、严防sql注入、校验参数、拦截深度分页。

ROW_NUMBER() 分页慢,90% 是索引没对齐
执行计划里出现 Sort 或 Table Scan,不是写法错,是 ORDER BY 字段压根没走索引。SQL Server 必须先排序再编号,没索引就硬扫全表+内存排序,5000 行就 20 秒很正常。
关键不是“加个索引”,而是索引结构必须和 OVER (ORDER BY ...) 完全一致:
- 如果写
ROW_NUMBER() OVER (ORDER BY created_at DESC, id DESC),就必须建CREATE INDEX IX_logs_time_id ON logs(created_at DESC, id DESC) - 若 WHERE 常带
status = 1,把status加到索引最左:CREATE INDEX IX_logs_status_time_id ON logs(status, created_at DESC, id DESC) - 避免在排序字段上用表达式,比如
ORDER BY DATEADD(day, 1, created_at)—— 索引直接失效
存储过程里拼 @OrderBy 字符串?先过 QUOTENAME 这关
动态排序字段、表名、列名直接拼进 SQL 字符串,等于给 SQL 注入开后门。哪怕只是内部系统,也别赌运气。
- 所有变量名拼接前必须套
QUOTENAME(@TableName)、QUOTENAME(@SortColumn),不能裸写 -
@PageIndex和@PageSize必须校验:IF @PageIndex ;<code>IF @PageSize > 100 SET @PageSize = 100 - 起始行计算别漏 +1:
DECLARE @StartRow INT = (@PageIndex - 1) * @PageSize + 1,否则第 1 页永远丢第 1 条 - 深度分页主动拦截:
IF (@PageIndex > 5000) RAISERROR('Deep page not allowed', 16, 1),比等它卡死强
OFFSET FETCH 比 ROW_NUMBER() 更快?不一定,看偏移量
OFFSET FETCH 在小偏移(OFFSET 100000 ROWS,SQL Server 得物理遍历 B+ 树前 10 万行节点——这比 ROW_NUMBER() 配合正确索引还慢。
ROW_NUMBER() 的优势其实在可控的深分页场景:
- 当排序字段有高效联合索引时,SQL Server 能用索引 Seek + Top N 优化,跳过大量中间行
- 而 OFFSET FETCH 没法跳过,只能线性推进
- 实测:100 万行表,查第 5000 页(每页 20 条),
ROW_NUMBER()+ 正确索引耗时 180ms;OFFSET 99980 ROWS耗时 2.4s
WHERE 里用 LIKE '%abc' 或函数?索引失效,ROW_NUMBER() 白搭
就算你把 ORDER BY 索引配得再完美,只要 WHERE 条件破坏了索引可用性,外层 ROW_NUMBER() 就只能在全表扫描结果上编号。
-
WHERE name LIKE '%张%'—— 左模糊,无法用name索引 -
WHERE UPPER(name) = 'ABC'—— 函数作用于列,索引失效 - 解决思路:要么改查询逻辑(如用全文索引替代
LIKE),要么把过滤字段也纳入联合索引(如status, name),让索引能覆盖 WHERE + ORDER BY
真正卡住性能的,往往不是 ROW_NUMBER() 本身,而是它暴露出来的底层索引缺陷和参数校验盲区。一个没 QUOTENAME 的字符串拼接,或一个漏掉 +1 的起始行计算,都可能让整页查询返回空或越界——这种问题在线上很难复现,但一出就是 P0。











