mysql深分页越翻越慢本质是limit offset,size需扫描并丢弃offset行,非sql错误而是设计使然;推荐子查询+主键关联或游标分页(where id > ?),要求排序字段有索引且避免select *。

为什么存储过程里用 LIMIT 会越来越慢
不是存储过程本身的问题,而是 LIMIT offset, size 在底层必须跳过前 offset 行才能取数据。当 offset 达到几十万时,MySQL 仍要扫描并丢弃大量行——哪怕有主键索引,也得按顺序遍历索引树找到第 N 个位置。实测中 LIMIT 999990, 10 耗时可能是 LIMIT 0, 10 的百倍以上。更麻烦的是,如果存储过程中拼接了动态 WHERE 条件但没走索引,就会退化成全表扫描,性能雪崩。
用主键/自增ID做游标分页(推荐首选)
绕过 OFFSET 的核心思路是:不依赖“第几页”,改用“从某 ID 之后取 N 条”。前提是排序字段(如 id)有索引且单调递增/递减,且查询条件稳定。
在存储过程中可这样写:
SELECT * FROM orders_history WHERE id > ? AND type = 8 ORDER BY id ASC LIMIT 20;
调用时传入上一页最后一条的 id 值(比如 1024567),下一页就自然从它之后开始。这种方式逻辑读极低,基本只走索引范围扫描。
- 必须确保
ORDER BY字段和WHERE中的游标字段一致,否则可能漏数据或重复 - 不能用于任意跳转(比如直接跳到第 100 页),只适合“下一页”“上一页”场景
- 如果业务要求支持跳页,需额外缓存每页首尾 ID 或用临时表预生成页码映射
存储过程里慎用 ROW_NUMBER() 或子查询分页
SQL Server 存储过程中常用 ROW_NUMBER() OVER (ORDER BY id) 配合 WHERE rn BETWEEN ? AND ?,但 MySQL 8.0 以下不支持窗口函数;而用子查询 (SELECT id FROM t ORDER BY id LIMIT ?, 1) 获取锚点 ID,在高并发下容易因执行计划不稳定导致延迟抖动。
常见坑点:
- MySQL 5.7 中子查询里的
LIMIT不能直接用在WHERE条件右侧,必须包一层派生表 - 存储过程参数若参与子查询(如
limit @offset, 1),可能触发参数嗅探失效,优化器误判为全表扫描 - 若表有
WHERE动态条件,又没对所有组合建联合索引,ROW_NUMBER()或子查询仍会先生成巨大中间结果集
真正影响性能的其实是索引设计,不是写法
再好的分页逻辑,没有匹配的索引也是空谈。比如查询 WHERE status = 1 AND create_time > '2025-01-01' ORDER BY id,只给 id 建主键索引远远不够。
必须根据实际 WHERE + ORDER BY 组合建联合索引:
- 把等值条件字段放前面(如
status),范围条件字段放后面(如create_time),排序字段(id)放最后 - 避免在索引字段上用函数或表达式,比如
WHERE DATE(create_time) = '2025-01-01'会让索引失效 - 如果经常按时间倒序分页,考虑建
(status, create_time DESC, id DESC)索引,让排序也能走索引
很多线上问题最后发现不是分页写得不对,而是压根没索引,或者索引顺序和查询模式错配——这点比选哪种分页语法重要十倍。











