在日常使用 MySQL 的过程中,存储过程是一个非常实用的功能——它本质上是一组预先编译好的 SQL 指令集,可以被反复调用,从而简化重复操作。然而,当你通过存储过程查询出一批结果后,往往需要逐条处理这些数据。这时,循环机制就变得至关重要。本文将详细介绍如何在 MySQL 存储过程中实现对查询结果的循环遍历与处理。
1. 建表准备
先搭建一个简单的实验场景。我们准备两张表:t_job 用于存储任务信息,t_task 用于记录每次任务执行的详细情况。具体的建表语句如下:
CREATE TABLE `t_job` ( `job_id` int(11) unsigned NOT NULL AUTO_INCREMENT, `job_name` varchar(50) DEFAULT NULL, `next_time` timestamp NULL DEFAULT NULL COMMENT '下次执行时间', `last_task` int(11) DEFAULT NULL, PRIMARY KEY (`job_id`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4; CREATE TABLE `t_task` ( `task_id` int(11) unsigned NOT NULL AUTO_INCREMENT, `start_time` datetime DEFAULT NULL, `end_time` datetime DEFAULT NULL, `status` tinyint(1) DEFAULT NULL, `job_id` int(11) NOT NULL, PRIMARY KEY (`task_id`) ) ENGINE=InnoDB AUTO_INCREMENT=25 DEFAULT CHARSET=utf8mb4;
2. 存储过程查询结果的处理
2.1 创建基础存储过程
先实现一个基础版本,该版本仅处理单条记录。逻辑非常简单:先查找出需要执行的 job,然后往 t_task 表中插入一条执行记录,同时更新 t_job 表中的下次执行时间和最近一次任务编号。
CREATE DEFINER=`root`@`%` PROCEDURE `p_sayn_job`()
BEGIN
#Routine body goes here...
DECLARE v_cnt INT;
DECLARE v_job_id INT;
SELECT count( 1 ) INTO v_cnt
FROM t_job j
WHERE j.next_time < SYSDATE();
IF v_cnt > 0 THEN
-- 插入数据
INSERT INTO t_task ( start_time, end_time, STATUS, job_id )
VALUES ( SYSDATE(), SYSDATE()+ 1, 1, v_job_id );
-- 更新数据
UPDATE t_job j
SET j.last_task = ( SELECT MAX( t.task_id ) FROM t_task t WHERE t.job_id = j.job_id ),
j.next_time = DATE_ADD( j.next_time, INTERVAL 1 DAY )
WHERE j.job_id = v_job_id;
END IF;
END
这个版本仅能处理一条记录,但如果满足条件的 job 有多条该怎么办?此时就需要引入循环机制了。
2.2 添加循环语句(基于游标实现)
这里的关键技术是游标(Cursor)。简单来说,游标就像是一个指针,能够逐行遍历结果集。下面我们来分解实现步骤。
首先使用 DECLARE 声明一个游标,并将需要循环的查询结果集绑定给它:
-- DECLARE语句声明游标 DECLARE jobs CURSOR FOR (SELECT j.job_id FROM t_job j WHERE j.next_time > SYSDATE());
接着声明一个变量,用于标记循环何时结束:
-- 声明变量 DECLARE v_finished int DEFAULT FALSE; -- 结束标识 DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_finished = TRUE;
然后打开游标,准备开始处理数据:
-- OPEN语句打开游标 OPEN jobs ;
实际的循环迭代部分,用 LOOP 结构包裹起来:
-- 循环迭代 jobs read_loop : LOOP END LOOP read_loop;
在循环体内,使用 FETCH 语句取出当前游标指向的一行数据,同时将游标指针下移一行:
-- 使用FETCH语句检索光标指向的下一行,并将光标移动到结果集中的下一行。 FETCH jobs into v_job_id;
如何判断循环应该终止?答案是利用之前定义的 v_finished 变量。当 FETCH 发现没有更多数据时,提前设置的 CONTINUE HANDLER 会自动将 v_finished 设置为 TRUE:
-- 使用v_finished变量来检查列表是否有id来终止循环。 IF v_finished THEN LEA VE read_loop; END IF;
在这段判断逻辑之后,你就可以放心地嵌入自己的业务处理代码了。最后务必使用 CLOSE 关闭游标,以释放关联的内存资源:
-- CLOSE语句以停用游标并释放与其关联的内存 CLOSE jobs;
将上述各个部分组合起来,就构成了一个完整的带循环的存储过程:
CREATE DEFINER=`root`@`%` PROCEDURE `p_sayn_job`()
BEGIN
#Routine body goes here...
DECLARE v_cnt INT;
DECLARE v_finished int DEFAULT FALSE;
DECLARE v_job_id INT;
-- DECLARE语句声明游标
DECLARE jobs CURSOR FOR
(SELECT j.job_id FROM t_job j WHERE j.next_time > SYSDATE());
-- 结束标识
DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_finished = TRUE;
SELECT count( 1 ) INTO v_cnt
FROM t_job j
WHERE j.next_time < SYSDATE();
IF v_cnt > 0 THEN
-- OPEN语句打开游标
OPEN jobs ;
-- 循环迭代 jobs
read_loop : LOOP
-- 使用FETCH语句检索光标指向的下一行,并将光标移动到结果集中的下一行。
FETCH jobs into v_job_id;
-- 使用v_finished变量来检查列表是否有id来终止循环。
IF v_finished THEN
LEA VE read_loop;
END IF;
-- 处理业务SQL放这里
INSERT INTO t_task ( start_time, end_time, STATUS, job_id )
VALUES ( SYSDATE(), SYSDATE()+ 1, 1, v_job_id );
UPDATE t_job j
SET j.last_task = ( SELECT MAX( t.task_id ) FROM t_task t WHERE t.job_id = j.job_id ),
j.next_time = DATE_ADD( j.next_time, INTERVAL 1 DAY )
WHERE j.job_id = v_job_id;
END LOOP read_loop;
-- CLOSE语句以停用游标并释放与其关联的内存
CLOSE jobs;
END IF;
END

2.3 保存并执行存储过程
编写完成后,调用存储过程验证其执行效果:
CALL p_sayn_job();

总结
游标的使用其实并不复杂,核心步骤可归纳为:声明游标、打开游标、循环取数、判断结束、关闭游标。借助上述方法,许多复杂的数据处理逻辑都能被逐一拆解并高效实现。希望本文能帮助你少走弯路,轻松掌握 MySQL 存储过程中的循环处理技巧。
