存储过程本身不产生独立进程,其cpu消耗归入调用线程,热点sql需通过performance_schema.events_statements_summary_by_digest定位,重点关注digest_text非select/insert的高耗时语句及子语句。

直接看 performance_schema.events_statements_summary_by_digest
存储过程本身不产生独立进程,它的 CPU 消耗会归入调用它的线程,并体现在 SQL 执行统计中。所以别在 SHOW PROCESSLIST 里找“存储过程名”,它只显示 CALL proc_name() 这一行,真正耗 CPU 的是过程体内的语句。
重点查这个视图:
SELECT DIGEST_TEXT AS query, SCHEMA_NAME AS db, SUM_TIMER_WAIT/1e9 AS time_sec, SUM_ROWS_EXAMINED AS rows, COUNT_STAR AS exec_count FROM performance_schema.events_statements_summary_by_digest WHERE DIGEST_TEXT LIKE 'CALL %' OR DIGEST_TEXT NOT LIKE 'SELECT %' OR DIGEST_TEXT NOT LIKE 'INSERT %' ORDER BY SUM_TIMER_WAIT DESC LIMIT 5;
注意:DIGEST_TEXT 是标准化后的语句摘要,CALL my_proc() 会被统一归类,但更关键的是——它下面的子语句(比如循环里的 UPDATE、SELECT INTO)也会单独出现在同一张表里,且 SUM_TIMER_WAIT 往往更高。所以排序后前几条大概率就是过程内部的热点 SQL。
- 如果某条
UPDATE或SELECT的time_sec显著高于其他语句,直接拿它的DIGEST_TEXT去反查原始 SQL(用performance_schema.events_statements_history_long或慢日志) -
rows值极大(如百万级)+time_sec高 → 典型的无索引循环扫描 -
exec_count高 + 单次time_sec低 → 存储过程被高频调用,哪怕单次不重,积少成多
检查存储过程是否隐式触发全表扫描
存储过程常含动态拼接、IF 分支、游标遍历等逻辑,容易绕过索引。尤其注意这些写法:
- 游标声明里用了
SELECT * FROM t WHERE ...,但WHERE条件列没索引 → 每次 fetch 都全表扫一遍 -
SELECT ... INTO @var语句未加LIMIT 1,且结果集可能为空或多行 → MySQL 会执行完整扫描再报错或截断 - 过程内用
CONCAT拼接条件导致索引失效,例如WHERE name = CONCAT('prefix', @suffix) - 参数类型和字段类型不一致,比如
INT参数传给VARCHAR字段 → 触发隐式转换,索引失效
验证方法:把过程里疑似有问题的 SQL 单独拿出来,用 EXPLAIN 看 type 是否为 ALL 或 index,key 是否为 NULL。
识别游标和循环导致的 CPU 密集型操作
MySQL 游标不是“一次取完”,而是每次 FETCH 都触发一次底层查询(尤其在未使用 READS SQL DATA 显式声明时)。如果循环体里还有 UPDATE 或函数调用,CPU 会线性增长。
典型高危模式:
-
DECLARE cur CURSOR FOR SELECT id FROM huge_table;→ 即使只取id,全表扫描仍发生 - 循环内执行
SELECT COUNT(*) FROM t WHERE x = @val→ 每次都走一次索引查找+计数,不如提前建汇总表 - 用
REPEAT ... UNTIL模拟 while,但退出条件依赖实时查询结果 → 可能无限循环或反复查询
快速定位:在 performance_schema.events_statements_history_long 中过滤 SQL_TEXT 含 FETCH、SELECT ... INTO、UPDATE 且 EVENT_NAME 为 statement/sql/select 或 update 的记录,按 TIMER_WAIT 排序。
临时禁用存储过程调用并观察 CPU 走势
这不是最终解法,但能快速验证猜想。如果确认某几个 CALL 是元凶,可在应用层或代理层(如 ProxySQL)临时拦截,或在数据库侧用以下方式限制:
- 对调用者账号回收执行权限:
REVOKE EXECUTE ON PROCEDURE db.proc_name FROM 'user'@'%'; - 用
pt-kill自动 kill 特定CALL语句:pt-kill --match-command Query --match-info "CALL proc_name" --kill - 在过程开头加硬编码判断,例如
IF @@hostname = 'prod-db-01' THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Disabled in prod'; END IF;
注意:不要直接删存储过程,除非你 100% 确认没有定时任务或旧代码还在调用它。线上环境最怕“以为删干净了,结果半夜某个 cron 又把它唤醒”。
存储过程的问题往往藏在“看起来合理”的循环和游标里,监控数据不会直接告诉你“第 7 行的 FETCH 在吃 CPU”,得靠 events_statements_summary_by_digest 把它从聚合数据里揪出来,再一层层剥开过程体验证。











