答案是sp_head::main_mem_root内存未释放所致;需查performance_schema中memory/sql/sp_head::main_mem_root用量超1gb即确认,且必须显式close游标、加异常处理、避免大结果集fetch。

MySQL 存储过程执行时报内存溢出,基本不是“内存不够”这种表层问题,而是 sp_head::main_mem_root 这块 SQL 层专属内存被游标、临时表或触发器持续占满,最终触发 OOM Killer 杀掉 mysqld。它不走 innodb_buffer_pool_size 管控,也不会随过程退出自动释放——关不掉游标,就等于内存永远挂着。
怎么确认是 sp_head::main_mem_root 吃光内存
别猜,直接查 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 LIKE 'memory/sql/sp_head%' - 重点盯
memory/sql/sp_head::main_mem_root这一行:如果 >1GB(尤其远超你的innodb_buffer_pool_size),八成就是它 - 配合
SHOW PROCESSLIST看有没有长期卡在executing或Sending data的连接,且对应 SQL 含DECLARE CURSOR FOR SELECT
游标必须显式 CLOSE,且异常路径也要覆盖
MySQL 不会在存储过程退出时自动释放游标占用的 main_mem_root 内存,哪怕你用 LEAVE 跳出循环,只要没 CLOSE,这块内存就一直驻留。
- 每个
DECLARE CURSOR cursor_name后,必须紧跟着配对的CLOSE cursor_name,且放在END之前 - 必须加异常处理:
DECLARE CONTINUE HANDLER FOR SQLEXCEPTION BEGIN CLOSE cursor_name; LEAVE proc_label; END; - 如果游标在嵌套块(如
BEGIN ... END)里声明,CLOSE也得写在同级块内,跨块无效 - 不要依赖“过程结束自动清理”——这是 MySQL 5.7/8.0 都未实现的假定
大表关联别硬 JOIN,改用分批主键拉取
存储过程中大表 JOIN 报内存溢出,90% 是因为 MySQL 被迫把整个右表加载进内存建哈希表。调 join_buffer_size 治标不治本,还可能引发并发 OOM。
- 适用前提:左右表都有可排序字段(如自增
id、created_at) - 第一步:分段查左表主键,用游标式分页(禁用
LIMIT OFFSET):SELECT id FROM orders WHERE id > ? ORDER BY id LIMIT 1000 - 第二步:用这批
id精准拉右表:SELECT o.*, u.name FROM orders o JOIN users u ON o.user_id = u.id WHERE o.id IN (1001,1002,...),IN列表长度建议 ≤1000 - 两个查询都必须在关联字段(
user_id、id)上建索引,否则IN会退化成全表扫描 - 检查
EXPLAIN输出,避免出现Using join buffer (Block Nested Loop)—— 有就说明没走索引,分批也白搭
临时表必须显式建索引,禁用 CREATE TEMPORARY TABLE SELECT
MySQL 中 CREATE TEMPORARY TABLE SELECT 会跳过索引定义阶段,后续大结果集 JOIN 或 ORDER BY 就容易爆内存;而 SELECT INTO 在 SQL Server 里同样危险,它隐式锁+无索引+日志膨胀。
- 正确做法:先
CREATE TEMPORARY TABLE #temp (...)显式声明结构,再CREATE INDEX关键字段(如CREATE INDEX IX_temp_status ON #temp (status_id)),最后INSERT INTO #temp SELECT ... - 避免在循环中反复向同一临时表
INSERT;大数据量场景下,每批处理完应DROP TEMPORARY TABLE #temp - 慎用 CTE 替代临时表:MySQL 8.0 默认强制物化,比带索引的临时表更占内存;仅当 CTE 只被引用一次、无聚合/窗口函数时才考虑
真正难缠的不是语法写错,而是 sp_head::main_mem_root 这种不释放、不报警、只等 OOM Killer 动手的隐性内存泄漏——它藏在游标没关、临时表没索引、分批逻辑漏索引这些细节里,一并发就线性爆炸。











