mysql 5.7及更早版本存储过程中limit不支持变量,需用concat拼接sql字符串并配合prepare/execute动态执行,且须校验整数参数防注入。

MySQL存储过程里不能直接用 LIMIT ? , ?
MySQL 5.7 及更早版本的存储过程中,LIMIT 子句不支持直接使用变量(比如 LIMIT offset, size 中的 offset 和 size),会报错 ERROR 1064: You have an error in your SQL syntax。这是最常卡住人的地方——你以为写对了,其实语法根本过不去。
解决办法是拼接 SQL 字符串 + PREPARE/EXECUTE 动态执行:
- 声明
sql_text字符串变量,用CONCAT()拼出完整查询语句(含数字型 offset/size) - 必须把数字转成字符串:
CONCAT('SELECT * FROM users LIMIT ', offset, ', ', size) -
PREPARE stmt FROM sql_text后紧接EXECUTE stmt,最后DEALLOCATE PREPARE stmt - 注意:拼接时要防 SQL 注入,所以只允许传入校验过的整数参数(如
IF offset )
如何安全传入分页参数并避免越界
用户可能传 page = 0 或 size = -10,不校验就会查出意外结果甚至全表扫描。存储过程里必须做显式检查:
- 用
DECLARE定义输入参数:IN p_page INT, IN p_size INT - 计算
offset:SET offset = GREATEST(0, (p_page - 1) * p_size)(假设页码从 1 开始) - 限制
p_size上限,比如SET size = LEAST(p_size, 100),防止恶意大值拖垮数据库 - 可选:用
SELECT COUNT(*)先查总条数,再决定是否返回空结果集(避免返回 0 行却不告诉调用方“已到底”)
MySQL 8.0+ 可以用窗口函数替代存储过程分页?
如果只是简单分页且不依赖过程逻辑(比如权限过滤、多表聚合),ROW_NUMBER() 确实更简洁,但要注意它不是存储过程的替代方案,而是另一条路:
SELECT * FROM (SELECT *, ROW_NUMBER() OVER (ORDER BY id) AS rn FROM users) t WHERE rn BETWEEN 21 AND 40- 性能上,当
ORDER BY字段无索引或数据量极大时,ROW_NUMBER()仍需扫描全部匹配行,比带索引的LIMIT动态拼接慢 - 无法在存储过程中直接用
ROW_NUMBER()做“通用分页封装”,因为子查询别名和外部 WHERE 无法参数化 - 真正省事的场景是应用层直连 MySQL 8.0+,且分页逻辑固定——这时干脆别写存储过程
返回分页结果时怎么一并带 total_count?
调用方通常需要知道“总共多少页”,但 MySQL 存储过程只能返回一个结果集。常见做法是用 OUT 参数或临时表,但最稳妥的是合并为单结果集:
- 用
UNION ALL把分页数据和总数拼在一起(需列数一致,可用NULL占位) - 更好的方式:用
SELECT子查询作为字段:SELECT *, (SELECT COUNT(*) FROM users WHERE status=1) AS total_count FROM ... LIMIT ... - 注意:子查询会为每行重复执行,若总数量不变,应提前算好存到变量,再 SELECT 时直接引用
- 如果业务要求严格一致性(分页期间数据可能变更),total_count 应和分页查询共用同一事务快照,此时必须用
SELECT COUNT(*)放在分页查询前,并加FOR UPDATE或合理隔离级别
动态 SQL + 参数校验 + 结果合并这三步漏掉任何一环,分页就可能返回错数据、被注入、或性能崩掉。尤其 PREPARE 的资源释放和异常处理容易被忽略——没写 DEALLOCATE 多次调用后会占满 prepared statement cache。











