存储过程里用 LIMIT offset, size 做深分页性能断崖下跌,因其必须扫描跳过前 N 行;优化关键在于用游标分页(Seek Method),即基于排序+唯一字段(如 create_time DESC, id DESC)和上一页最后值(last_create_time、last_id)构造 WHERE 条件,彻底规避 offset。

存储过程里直接用 LIMIT offset, size 做深分页,性能会断崖式下跌——不是语法错,而是执行逻辑本身决定了它必须跳过前 N 行。优化关键不是“怎么写存储过程”,而是“怎么绕过 LIMIT 的跳过机制”。
为什么存储过程里用 LIMIT 会更慢?
很多人误以为把 LIMIT 1000000, 10 包进存储过程就能缓存或加速,其实不会。MySQL 在存储过程中执行 SQL 时,LIMIT 的语义完全不变:仍需扫描并丢弃前 100 万行。更糟的是,如果存储过程里拼接了动态 WHERE 条件但没走索引,还可能触发全表扫描+临时排序,耗时翻倍。
- 存储过程不改变 SQL 执行计划,只封装调用逻辑
- 若参数未参与索引字段过滤(比如
WHERE user_id = in_user_id但没建user_id索引),性能雪上加霜 - 无法利用查询缓存(MySQL 8.0+ 已移除),预编译也救不了
LIMIT的本质缺陷
在存储过程中用游标分页(Seek Method)
这是最推荐的方式,尤其适合 API 分页、下拉加载等场景。核心是把“第 N 页”转换成“从某条记录之后取 10 条”,完全避开 offset。
- 要求排序字段 + 唯一字段组合能唯一标识一行(如
create_time DESC, id DESC) - 存储过程参数需接收上一页最后一条的
last_create_time和last_id - WHERE 条件必须写成:
(create_time —— 注意大小于号方向要和ORDER BY严格一致 - 示例片段:
SELECT id, name, create_time FROM orders WHERE user_id = p_user_id AND ((create_time
在存储过程中用延迟关联(Deferred Join)
当业务强制要求支持“跳转到第 500 页”这类页码输入时,只能用延迟关联。重点在于子查询必须只查主键,并命中覆盖索引。
- 先确保有联合索引,例如:
CREATE INDEX idx_user_time ON orders(user_id, create_time); - 子查询里不能出现非索引字段,否则回表开销照旧;
SELECT id FROM orders WHERE user_id = p_user_id ORDER BY create_time DESC LIMIT p_offset, p_size必须能走该索引 - 主查询用
JOIN关联原表,而非IN(MySQL 对大 IN 列表优化差,易退化为 N+1 查询) - 示例结构:
SELECT o.* FROM orders o INNER JOIN ( SELECT id FROM orders WHERE user_id = p_user_id ORDER BY create_time DESC LIMIT p_offset, p_size ) tmp ON o.id = tmp.id;
容易被忽略的细节
存储过程里做深分页,最容易栽在“看似合理”的地方:
- 参数名和字段名大小写混用(尤其在 Linux MySQL 中),导致索引失效
- 传入的
p_offset是 INT,但实际 offset 超过 2147483647(int 最大值),变成负数或截断,查询结果错乱 - 没对
last_create_time做 NULL 判断,首次调用时条件写成create_time ,整条 WHERE 失效 - ORDER BY 字段类型和索引顺序不一致(比如索引是
(user_id, create_time DESC),但 SQL 写成ORDER BY create_time ASC),导致索引无法用于排序











