为何MySQL执行简单SELECT @nb, @id_start查询时出现挂起?
批量处理大表时存储过程卡在
SELECT @nb, @id_start(Sending to client状态) 我正在处理一张包含8000多万行数据的表,通过存储过程批量更新数据,使用mysql CLI客户端调用执行。过程中通过SELECT @nb, @id_start输出处理进度,但发现过程会突然停止推进。Adminer显示该查询状态如下:
| Command | Time | State | Info |
|---|---|---|---|
| Query | 28370 | Sending to client | SELECT @nb, @id_start |
该查询已运行28370秒,杀死连接并重启mysql CLI后,处理能恢复正常。
执行的SQL脚本如下:
DROP PROCEDURE IF EXISTS `LoopProcedure`; DELIMITER $$ CREATE PROCEDURE LoopProcedure() BEGIN SET @id_start := 24978724; SELECT @id_start := `cursor` FROM `temporary` WHERE `id` = 1; SET @nb := 0; loop_label: LOOP -- 取100条数据中的最大emailId作为批次结束值 SELECT @id_end := MAX(`emailId`) FROM ( SELECT `emailId` FROM `BuyPacker_Email` WHERE `emailId` > @id_start ORDER BY `emailId` LIMIT 100 ) as t; -- 无更多数据则退出循环 IF @id_end IS NULL OR @id_end = @id_start THEN LEAVE loop_label; END IF; -- 更新计数 SET @nb := (@nb + 100); -- 更新crm_contact表 UPDATE `crm_contact`, `BuyPacker_Email` SET `crm_contact`.`email_status` = "VALID_EVENT" WHERE (`crm_contact`.`email_status` IN ("VALID", "NOT_VALID", "UNVERIFIED", "INVALID_SMTP") OR `crm_contact`.`email_status` IS NULL) AND `BuyPacker_Email`.`emailId` > @id_start AND `BuyPacker_Email`.`emailId` <= @id_end AND (`BuyPacker_Email`.`emailClick` IS NOT NULL OR `BuyPacker_Email`.`emailOpen` IS NOT NULL) AND `crm_contact`.`email` = `BuyPacker_Email`.`emailEmail`; -- 更新email_list_recipient表 UPDATE `email_list_recipient`, `BuyPacker_Email` SET `email_list_recipient`.`email_status` = "VALID_EVENT" WHERE `email_list_recipient`.`email_status` IN ("VALID", "NOT_VALID", "UNVERIFIED", "INVALID_SMTP") AND `BuyPacker_Email`.`emailId` > @id_start AND `BuyPacker_Email`.`emailId` <= @id_end AND (`BuyPacker_Email`.`emailClick` IS NOT NULL OR `BuyPacker_Email`.`emailOpen` IS NOT NULL) AND `BuyPacker_Email`.`emailEmail` COLLATE utf8mb4_unicode_ci = `email_list_recipient`.`email`; -- 更新游标记录 UPDATE `temporary` SET `cursor` = @id_start WHERE `id` = 1; -- 推进游标 SET @id_start := @id_end; -- 输出进度 SELECT @nb, @id_start; -- 给服务器留处理时间 DO SLEEP(0.2); END LOOP; END$$ DELIMITER ; CALL LoopProcedure(); DROP PROCEDURE `LoopProcedure`;
环境配置:
- MySQL服务器:Azure Database for MySQL单服务器
- mysql CLI客户端:运行在Azure容器实例中
- 两者处于同一虚拟网络(VNet)内
问题原因分析
卡在Sending to client状态通常意味着MySQL服务器已经完成了SELECT @nb, @id_start的查询处理,但无法将结果发送给客户端。结合环境和脚本,可能的原因包括:
- 客户端缓冲区阻塞:mysql CLI默认缓存查询结果,循环中频繁输出小结果集会导致客户端缓冲区累积阻塞,无法及时接收新结果,进而让服务器端一直处于发送等待状态。
- 网络传输异常:即使在同一VNet内,容器实例的网络连接可能出现临时停滞,比如端口资源耗尽、TCP滑动窗口异常等。
- 频繁交互开销累积:每次循环都执行
SELECT输出进度,会产生大量小结果集,增加客户端与服务器的交互开销,长期运行后容易触发阻塞问题。
解决建议
1. 调整进度输出频率,减少交互次数
不要每次循环都输出进度,改为每N次循环输出一次,比如每100次循环输出一次:
-- 在存储过程开头初始化计数器 SET @progress_counter := 0; -- 替换原有的SELECT @nb, @id_start; SET @progress_counter := @progress_counter + 1; IF @progress_counter % 100 = 0 THEN SELECT @nb, @id_start; END IF;
2. 优化mysql CLI启动参数
启动CLI时添加--quick参数,禁用客户端结果集缓存,让服务器发送结果后立即释放资源,避免缓冲区阻塞:
mysql -u username -p --quick < your_script.sql
3. 替换进度输出为日志表写入
完全去掉客户端进度输出,改为将进度写入专门的日志表,避免主流程的交互阻塞:
首先创建日志表:
CREATE TABLE IF NOT EXISTS process_log ( log_id INT AUTO_INCREMENT PRIMARY KEY, processed_rows INT NOT NULL, current_id BIGINT NOT NULL, log_time DATETIME DEFAULT CURRENT_TIMESTAMP );
然后在循环中替换SELECT @nb, @id_start;为:
INSERT INTO process_log(processed_rows, current_id) VALUES(@nb, @id_start);
后续可通过查询process_log表实时查看进度。
4. 优化批量更新的索引效率
- 确认
BuyPacker_Email表的emailId字段为主键或存在唯一索引,保证批次范围查询的效率 - 给
crm_contact.email、email_list_recipient.email字段添加合适的索引,减少更新时的匹配开销 - 将隐式连接改为显式
JOIN写法,提升语句可读性和执行效率
内容的提问来源于stack exchange,提问作者syl.fabre
相关产品推荐
相关产品推荐

