游标
导学
当需要在存储过程中逐行处理查询结果时,游标(Cursor)是唯一的选择。它像文件指针一样,让你可以一行一行地遍历结果集。
定义
游标(Cursor):一种数据库对象,用于在存储过程或函数中逐行遍历 SELECT 查询的结果集。游标允许对每一行进行单独处理。
核心语法
DECLARE cursor_name CURSOR FOR SELECT_statement;
OPEN cursor_name;
FETCH cursor_name INTO var1, var2, ...;
CLOSE cursor_name;
完整示例一:遍历员工并累加成绩
当前数据状态
首先建立示例表并插入数据:
CREATE DATABASE IF NOT EXISTS school;
USE school;
DROP TABLE IF EXISTS employees;
CREATE TABLE employees (
emp_id INT PRIMARY KEY AUTO_INCREMENT,
emp_name VARCHAR(20),
dept VARCHAR(20),
score DECIMAL(5,2)
);
INSERT INTO employees (emp_name, dept, score) VALUES
('大翔', '技术部', 100),
('白歌', '技术部', NULL);
当前 employees 表数据:
| emp_id | emp_name | dept | score |
|---|---|---|---|
| 1 | 大翔 | 技术部 | 100 |
| 2 | 白歌 | 技术部 | NULL |
操作语句
创建使用游标累加成绩的存储过程:
DELIMITER //
CREATE PROCEDURE CalcTotalScore()
BEGIN
DECLARE done INT DEFAULT FALSE;
DECLARE v_score DECIMAL(5,2);
DECLARE v_total DECIMAL(10,2) DEFAULT 0;
-- 1. 声明游标
DECLARE cur CURSOR FOR SELECT score FROM employees;
-- 2. 声明继续处理器:当没有更多行时设置 done = TRUE
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
-- 3. 打开游标
OPEN cur;
-- 4. 循环读取
read_loop: LOOP
FETCH cur INTO v_score;
IF done THEN
LEAVE read_loop;
END IF;
IF v_score IS NOT NULL THEN
SET v_total = v_total + v_score;
END IF;
END LOOP;
-- 5. 关闭游标
CLOSE cur;
SELECT v_total AS 总分;
END //
DELIMITER ;
-- 调用
CALL CalcTotalScore();
操作后的结果
返回:
| 总分 |
|---|
| 100 |
结果解读
执行步骤拆解:
| 步骤 | 操作 | v_score | v_total |
|---|---|---|---|
| 初始 | 初始化变量 | NULL | 0 |
| 第1次FETCH | 读取第1行 | 100 | 100 |
| 第2次FETCH | 读取第2行 | NULL | 100(NULL不加入) |
| 第3次FETCH | 无更多行,触发 NOT FOUND | NULL | 100 |
DECLARE cur CURSOR FOR SELECT score FROM employees;声明游标,关联查询语句DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;定义处理器:当游标没有更多行时,自动设置done = TRUEOPEN cur;打开游标,执行关联的查询FETCH cur INTO v_score;读取当前行数据到变量- 当
FETCH不到数据时,NOT FOUND处理器触发,done变为TRUE CLOSE cur;关闭游标,释放资源
完整示例二:带条件的游标遍历
当前数据状态
基于 employees 表:
| emp_id | emp_name | dept | score |
|---|---|---|---|
| 1 | 大翔 | 技术部 | 100 |
| 2 | 白歌 | 技术部 | NULL |
操作语句
创建使用游标分类处理的存储过程:
DELIMITER //
CREATE PROCEDURE CategorizeEmployees()
BEGIN
DECLARE done INT DEFAULT FALSE;
DECLARE v_id INT;
DECLARE v_name VARCHAR(20);
DECLARE v_score DECIMAL(5,2);
DECLARE cur CURSOR FOR
SELECT emp_id, emp_name, score FROM employees;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
-- 创建临时表存储分类结果
CREATE TEMPORARY TABLE IF NOT EXISTS temp_category (
emp_id INT,
emp_name VARCHAR(20),
category VARCHAR(10)
);
TRUNCATE TABLE temp_category;
OPEN cur;
read_loop: LOOP
FETCH cur INTO v_id, v_name, v_score;
IF done THEN
LEAVE read_loop;
END IF;
IF v_score >= 90 THEN
INSERT INTO temp_category VALUES (v_id, v_name, '优秀');
ELSEIF v_score IS NULL THEN
INSERT INTO temp_category VALUES (v_id, v_name, '未评分');
ELSE
INSERT INTO temp_category VALUES (v_id, v_name, '普通');
END IF;
END LOOP;
CLOSE cur;
SELECT * FROM temp_category;
DROP TEMPORARY TABLE IF EXISTS temp_category;
END //
DELIMITER ;
-- 执行分类
CALL CategorizeEmployees();
操作后的结果
返回:
| emp_id | emp_name | category |
|---|---|---|
| 1 | 大翔 | 优秀 |
| 2 | 白歌 | 未评分 |
结果解读
- 游标遍历
employees表,每行包含emp_id、emp_name和score - 根据
score值将分类结果写入临时表 - 100分及以上为优秀,NULL 为未评分,其余为普通
游标 vs 集合操作
完整示例:同样的逻辑用集合操作实现
当前数据状态
基于 employees 表:
| emp_id | emp_name | dept | score |
|---|---|---|---|
| 1 | 大翔 | 技术部 | 100 |
| 2 | 白歌 | 技术部 | NULL |
操作语句
用一条 INSERT ... SELECT 替代游标逐行处理:
-- 创建临时表存储分类结果
CREATE TEMPORARY TABLE IF NOT EXISTS temp_category_set (
emp_id INT,
emp_name VARCHAR(20),
category VARCHAR(10)
);
TRUNCATE TABLE temp_category_set;
-- 用集合操作一次性完成分类(替代游标逐行 FETCH + INSERT)
INSERT INTO temp_category_set
SELECT emp_id, emp_name,
CASE
WHEN score >= 90 THEN '优秀'
WHEN score IS NULL THEN '未评分'
ELSE '普通'
END
FROM employees;
-- 查看结果
SELECT * FROM temp_category_set;
DROP TEMPORARY TABLE IF EXISTS temp_category_set;
操作后的结果
| emp_id | emp_name | category |
|---|---|---|
| 1 | 大翔 | 优秀 |
| 2 | 白歌 | 未评分 |
结果解读
| 场景 | 游标方式 | 集合方式(推荐) |
|---|---|---|
| 累加 | 逐行 FETCH + 累加 | SELECT SUM(score) |
| 条件分类 | 逐行判断 + INSERT | INSERT ... SELECT ... CASE WHEN ... |
| 批量更新 | 逐行 UPDATE | UPDATE ... CASE WHEN ... |
- 集合操作利用数据库引擎的优化能力,通常比游标快得多
- 游标逐行处理,增加了网络/CPU 开销
- 能用一条 SQL 解决的,绝不要用游标
完整示例三:游标遍历多列数据
当前数据状态
基于 employees 表:
| emp_id | emp_name | dept | score |
|---|---|---|---|
| 1 | 大翔 | 技术部 | 100 |
| 2 | 白歌 | 技术部 | NULL |
操作语句
创建使用游标处理多列的存储过程:
DELIMITER //
CREATE PROCEDURE GetEmployeeReport()
BEGIN
DECLARE done INT DEFAULT FALSE;
DECLARE v_name VARCHAR(20);
DECLARE v_dept VARCHAR(20);
DECLARE v_score DECIMAL(5,2);
-- 创建临时表存储结果
CREATE TEMPORARY TABLE IF NOT EXISTS temp_report (
emp_name VARCHAR(20),
dept VARCHAR(20),
score DECIMAL(5,2),
remark VARCHAR(20)
);
TRUNCATE TABLE temp_report;
DECLARE cur CURSOR FOR
SELECT emp_name, dept, score FROM employees ORDER BY score DESC;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
OPEN cur;
read_loop: LOOP
FETCH cur INTO v_name, v_dept, v_score;
IF done THEN
LEAVE read_loop;
END IF;
-- 根据分数添加评语
IF v_score >= 90 THEN
INSERT INTO temp_report VALUES (v_name, v_dept, v_score, '优秀');
ELSEIF v_score IS NULL THEN
INSERT INTO temp_report VALUES (v_name, v_dept, v_score, '未评分');
ELSE
INSERT INTO temp_report VALUES (v_name, v_dept, v_score, '普通');
END IF;
END LOOP;
CLOSE cur;
SELECT * FROM temp_report;
DROP TEMPORARY TABLE IF EXISTS temp_report;
END //
DELIMITER ;
-- 调用
CALL GetEmployeeReport();
操作后的结果
返回:
| emp_name | dept | score | remark |
|---|---|---|---|
| 大翔 | 技术部 | 100 | 优秀 |
| 白歌 | 技术部 | NULL | 未评分 |
结果解读
- 游标
FETCH多列数据时,INTO子句的变量顺序和SELECT列的顺序必须一致 - 本例展示了游标在逐行处理时,可以结合流程控制做复杂逻辑
- 结果按分数从高到低排列,并标注了评语
常见误区
| 误区 | 正解 |
|---|---|
| 游标比集合操作快 | 恰恰相反。游标逐行处理,网络/CPU 开销大。集合操作是数据库的强项。 |
| 游标可以向前向后移动 | MySQL 5.7 的游标只能向前(FETCH NEXT),不能向后或随机访问。 |
| 不关闭游标没关系 | 游标占用服务器资源,必须在结束时关闭。 |
面试考点
Q:游标的优缺点?
优点:可以逐行处理结果集,适合复杂逐行逻辑。缺点:性能差,逐行处理违背了 SQL 的集合操作思想。能用集合操作替代的不要用游标。
Q:MySQL 游标有什么限制?
- 只读,不能通过游标修改数据;2. 只能向前移动(FETCH NEXT);3. 一次只能打开一个游标进行 FETCH(在存储过程中);4. 游标必须在
DECLARE变量之后、HANDLER之前声明。
Q:NOT FOUND Handler 的作用?
当
FETCH没有更多行时,MySQL 会抛出一个NOT FOUND条件。DECLARE CONTINUE HANDLER FOR NOT FOUND捕获这个条件,设置标志变量,让循环正常退出而不是报错。
小结
- 游标用于在存储过程中逐行遍历结果集
- 使用步骤:DECLARE → OPEN → FETCH → CLOSE
NOT FOUNDHandler 用于检测结果集结束- 游标性能差,能用集合操作(JOIN、子查询、CASE)替代的不要用游标
下一章引子:存储过程和函数是主动调用的程序单元,而触发器则是被动触发的——当表发生 INSERT、UPDATE、DELETE 时自动执行。