根本原因是sp_head::main_mem_root内存未释放;需查performance_schema中memory/sql/sp_head::main_mem_root用量超1GB即确认,且必须显式CLOSE游标、加异常处理器、避免大结果集FETCH。

根本原因不是循环本身,而是每次 FETCH 都往 sp_head::main_mem_root 里追加内存,且不复用、不释放——只要游标没 CLOSE,这块内存就一直挂着。
怎么确认真是 sp_head::main_mem_root 吃光内存
别看 SHOW STATUS 或 free -h,直接查 performance_schema:
- 先确保监控开着:
SELECT VARIABLE_VALUE FROM performance_schema.global_variables WHERE VARIABLE_NAME = 'performance_schema_instrument',返回值里必须含memory/% = COUNTED - 再跑:
SELECT EVENT_NAME, CURRENT_NUMBER_OF_BYTES_USED FROM performance_schema.memory_summary_global_by_event_name WHERE EVENT_NAME = 'memory/sql/sp_head::main_mem_root' - 如果值 >1GB(尤其远超你的
innodb_buffer_pool_size),基本坐实 - 配合
SHOW PROCESSLIST看有没有状态为Sending data或executing的长时连接,且 SQL 含DECLARE CURSOR FOR SELECT
CLOSE cursor_name 必须显式写,且异常路径也要覆盖
MySQL 5.7 不会在存储过程退出时自动释放游标内存,哪怕你用了 LEAVE、RETURN 或自然结束。漏关一次,每次调用就多挂一份内存。
- 每个
DECLARE cursor_name CURSOR FOR SELECT后,必须紧跟着配对的CLOSE cursor_name,且放在END前 - 必须加异常处理器:
DECLARE CONTINUE HANDLER FOR SQLEXCEPTION BEGIN CLOSE cursor_name; LEAVE proc_label; END; - 如果游标在嵌套
BEGIN ... END块中声明,CLOSE也得写在同级块内,跨块无效 - 禁止在循环里反复
OPEN同一个游标却不CLOSE——“下次覆盖”不成立
大结果集 FETCH 是隐形炸弹
游标不限大小,但每次 FETCH 都会把整行内容拷贝进 main_mem_root,字段越多、字符串越长,单次开销越大。拉 10 万行,可能瞬间吃掉 500MB+。
- 业务允许的话,把遍历逻辑提到应用层(如 Python/Java),数据库只做分页查询
- 若必须在存储过程中处理,改用主键分段拉取:
SELECT id FROM t WHERE id > ? ORDER BY id LIMIT 1000,再用这批 ID 精准 JOIN - 游标查询里禁用
SELECT *,只选必要字段;避免多层CONCAT()拼接超长字符串 - 不要依赖“过程结束自动清理”——这是 MySQL 5.7 从未实现的假定
最容易被忽略的一点:sp_head::main_mem_root 内存不走 innodb_buffer_pool_size 管控,也不随连接关闭释放,只随存储过程实例生命周期存在。并发调用越多,泄漏越快,而问题往往在压测后期才爆发。


















