SQL Server分页必须用ROW_NUMBER()+唯一排序,禁用OFFSET-FETCH;MySQL禁用LIMIT offset,size,改用延迟关联或Keyset分页;总数统计应异步缓存或仅返回“是否有下一页”。

直接用 LIMIT offset, size 或 OFFSET-FETCH 在存储过程中写死分页逻辑,响应速度反而会随页码增大急剧下降——这不是写法问题,是执行计划和索引利用方式被破坏了。
SQL Server 存储过程必须用 ROW_NUMBER() + 显式唯一排序
哪怕你用的是 SQL Server 2012+,也别在生产存储过程中依赖 OFFSET-FETCH 做通用分页。它在大偏移量下仍会触发聚集索引扫描,且无法跳过已读行;而 ROW_NUMBER() 虽需三层嵌套,但只要控制好 WHERE 下推和排序兜底,就能稳定走索引查找。
- ORDER BY 必须包含至少一个唯一列(如
ID或CreateTime DESC, ID DESC),否则相同时间戳的多条记录会导致行号错乱、结果重复或丢失 - WHERE 条件不能写在外层,必须下推到最内层子查询,否则
@start_row计算会失准(比如过滤掉 50% 数据后还按原始总行数算页) - 避免在
ROW_NUMBER()的 OVER 子句里用函数,例如ORDER BY YEAR(CreateTime),这会让索引完全失效
MySQL 存储过程禁用 LIMIT offset, size
在存储过程中写 LIMIT p_offset, p_size 是性能陷阱:每次调用都会重新解析、无法复用执行计划,offset 超过 10 万后,优化器大概率放弃索引,退化为全表扫描或文件排序。
- 改用延迟关联:子查询只查主键,外层 JOIN 原表,例如
SELECT o.* FROM orders o INNER JOIN (SELECT id FROM orders WHERE status = ? ORDER BY status, create_time LIMIT ?, ?) AS tmp USING(id) - 排序字段必须落在同一联合索引中,且顺序与
ORDER BY严格一致(如索引是(status, create_time),就不能写成ORDER BY create_time, status) - 参数类型必须和字段类型一致,
p_status是TINYINT,就别传字符串,否则触发隐式转换,让idx_status_created彻底失效
Keyset 分页才是千万级数据的可靠选择
所谓“第 N 页”,本质是业务幻觉;真实高效的做法是记住上一页最后一条记录的排序键值,用范围查询代替偏移计算。这对存储过程尤其友好——你可以用 INOUT 参数传入/传出游标值,比应用层维护更可靠。
- MySQL 示例:
WHERE create_time > @last_time OR (create_time = @last_time AND id > @last_id),配合ORDER BY create_time, id - SQL Server 示例:
WHERE sort_column > @lastValue,要求sort_column单调且有索引支持;若含 NULL,得补AND sort_column IS NOT NULL - 不能用自增 ID 直接当游标,除非业务确认无删除、无跳号;时间字段必须拼上 ID 防止毫秒级重复
最容易被忽略的一点:总数统计不要放在分页主查询里。COUNT(*) 在千万级表上可能比取数据还慢,而且多数前端根本不需要精确总数——用异步任务更新缓存值,或返回“是否有下一页”布尔标记,才是实际可落地的方案。

















