📌 今日关键词:游标、逐行处理、结果集遍历、存储过程、MySQL进阶
大家好呀!我是数据库小学妹👋
前面我们学的SQL都是集合操作——一次对整张表或整个结果集进行操作。但有时候,我们需要一行一行地处理数据:
- 对每个用户的积分进行不同规则的累加
- 把某个字段的值拆分成多行插入另一张表
- 批量发送邮件,每封内容根据用户定制
这时候,普通的SQL就不够用了。数据库给我们准备了 “游标” ——就像一只“逐行扫描仪”,可以一行一行地读取、处理和操作数据。游标看起来有点“高级”,但别担心,小学妹会用生活化的例子,带你从原理到实战,彻底搞懂这个数据库中的“智能指针”!
一、什么是游标?
游标(Cursor) 是一种逐行处理结果集的机制。你可以把它想象成一个可移动的“指针”,它指向结果集中的某一行,然后你可以移动指针,每次处理一行。
💡 类比:你有一摞试卷(结果集),你想逐张批改。游标就是你的“手”,可以一张一张拿起来看,改完后放一边,再拿下一张。
📢什么时候用游标?
- 需要对每一行执行不同的计算(不能用一个UPDATE搞定)
- 需要把一行数据拆成多行(如把“苹果,香蕉”拆成两行)
- 需要调用外部程序(如发邮件、调API)对每行数据单独操作
🔍游标的核心思想:
将查询结果集视为可移动的“数据队列”,通过游标指针逐行读取、处理,甚至修改数据,实现精细化的流程控制! 它打破了SQL“集合操作”的限制,让数据库也能像编程一样逐行处理逻辑!
⚠️ 警告:游标性能较差,能不用就不用。大数据量下尽量避免,优先用集合操作(
UPDATE、INSERT...SELECT)。
二、游标的基本步骤(4步法)
在存储过程中,游标的使用分为四步:
| 步骤 | 关键字 | 说明 |
|---|---|---|
| 1. 声明游标 | DECLARE 游标名 CURSOR FOR SELECT... |
定义游标要遍历的结果集 |
| 2. 打开游标 | OPEN 游标名 |
初始化,准备读取数据 |
| 3. 提取数据 | FETCH 游标名 INTO 变量列表 |
获取当前行的值,并存储到变量 |
| 4. 关闭游标 | CLOSE 游标名 |
释放资源 |
通常还需要一个 NOT FOUND 处理程序,用于判断是否已经取完所有行。
三、游标的使用场景
📊 批量更新+条件判断:
- 场景:更新员工工资时,根据部门不同,涨幅规则不同。
- 方案:用游标遍历员工表,对每条记录判断部门后计算新工资。
📊数据导出与转换:
- 场景:需要将数据库中的订单数据逐条导出到Excel,并添加自定义备注。
- 方案:通过游标逐行读取订单,生成格式化数据。
📊复杂报表生成:
- 场景:需要按学生分数段生成统计报表,并附加评语(如“优秀”“需加强”)。
- 方案:游标遍历成绩表,动态计算评语并写入临时表。
📊事务中的补偿逻辑:
- 场景:转账时,若某条记录失败,需回滚并标记错误日志。
- 方案:用游标逐笔处理转账,异常时通过游标定位到问题记录。
四、实战:用游标给每个用户增加不同积分
假设有一个用户表 users(id, name, score),需求是:
- 金牌会员(
level='gold')增加 100 积分 - 银牌会员(
level='silver')增加 50 积分 - 普通会员(
level='normal')增加 10 积分
如果用普通SQL,需要写三个 UPDATE。这里用游标演示“逐行判断”的过程。
🌰创建示例表
CREATE TABLE users (
id INT PRIMARY KEY,
name VARCHAR(20),
level VARCHAR(10),
score INT
);
INSERT INTO users VALUES (1, '小明', 'gold', 0), (2, '小红', 'silver', 0), (3, '小刚', 'normal', 0);
📝编写存储过程 + 游标
DELIMITER //
CREATE PROCEDURE update_score_cursor()
BEGIN
-- 声明变量,用于存储当前行的数据
DECLARE done INT DEFAULT 0;
DECLARE v_id INT;
DECLARE v_level VARCHAR(10);
DECLARE v_score INT;
-- 声明游标
DECLARE cur CURSOR FOR SELECT id, level FROM users;
-- 声明当游标读取不到数据时,设置 done = 1
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;
-- 打开游标
OPEN cur;
-- 开始循环
read_loop: LOOP
-- 提取一行数据到变量
FETCH cur INTO v_id, v_level;
-- 如果没数据了,退出循环
IF done THEN
LEAVE read_loop;
END IF;
-- 根据等级计算新积分
CASE v_level
WHEN 'gold' THEN SET v_score = 100;
WHEN 'silver' THEN SET v_score = 50;
ELSE SET v_score = 10;
END CASE;
-- 更新该用户的积分
UPDATE users SET score = v_score WHERE id = v_id;
END LOOP;
-- 关闭游标
CLOSE cur;
END //
DELIMITER ;
📅调用游标
CALL update_score_cursor();
SELECT * FROM users;
结果:
| id | name | level | score |
|---|---|---|---|
| 1 | 小明 | gold | 100 |
| 2 | 小红 | silver | 50 |
| 3 | 小刚 | normal | 10 |
五、更实用的例子:将逗号分隔的字符串拆分为多行
假设有一张 student_courses 表,其中 courses 字段存着“数学,语文,英语”,你需要把它拆分成多行插入到 student_course 表中。
-- 原始表
CREATE TABLE student_courses (
student_id INT,
courses VARCHAR(100)
);
INSERT INTO student_courses VALUES (1, '数学,语文,英语');
-- 目标表
CREATE TABLE student_course (
student_id INT,
course_name VARCHAR(20)
);
游标 + 字符串处理函数实现拆分(略简化版):
DELIMITER //
CREATE PROCEDURE split_courses()
BEGIN
DECLARE done INT DEFAULT 0;
DECLARE v_stu_id INT;
DECLARE v_courses VARCHAR(100);
DECLARE v_course VARCHAR(20);
DECLARE comma_pos INT;
DECLARE cur CURSOR FOR SELECT student_id, courses FROM student_courses;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;
OPEN cur;
read_loop: LOOP
FETCH cur INTO v_stu_id, v_courses;
IF done THEN LEAVE read_loop; END IF;
-- 循环取出每个逗号前的子串
WHILE LENGTH(v_courses) > 0 DO
SET comma_pos = LOCATE(',', v_courses);
IF comma_pos = 0 THEN
SET v_course = v_courses;
SET v_courses = '';
ELSE
SET v_course = SUBSTRING(v_courses, 1, comma_pos - 1);
SET v_courses = SUBSTRING(v_courses, comma_pos + 1);
END IF;
INSERT INTO student_course VALUES (v_stu_id, TRIM(v_course));
END WHILE;
END LOOP;
CLOSE cur;
END //
DELIMITER ;
调用后,student_course 表会生成三行:(1, '数学'), (1, '语文'), (1, '英语')。
六、游标避坑指南
💢坑1:游标滥用导致性能雪崩:
- 避坑:能用集合操作(如UPDATE/DELETE语句)解决的,绝不使用游标!游标是“最后手段”。
- 优化技巧:若必须用,限制处理行数,或分批次处理。
💢坑2:资源泄漏:
- 避坑:必须显式关闭游标!未关闭的游标会占用数据库连接资源,导致系统卡顿。
- 最佳实践:在存储过程结尾或异常处理中确保关闭游标。
💢坑3:嵌套游标陷阱:
- 避坑:避免在循环内嵌套游标,极易导致性能噩梦。
- 替代方案:用临时表或变量暂存数据,减少嵌套。
💢坑4:忽略事务影响:
- 避坑:游标操作通常在事务中,需注意隔离级别(如可重复读 vs 读已提交),避免数据不一致。
七、今日学习心得
- 游标逐行处理,适合那些“每一行都要特殊对待”的场景
- 四步法:声明 → 打开 → 提取 → 关闭,别忘了
NOT FOUND处理 - 性能杀手,慎用:优先思考是否能用集合操作代替
👋 我是数据库小学妹一个用设计师思维学数据库的转行人。我们一起,把复杂的技术变得简单有趣!💕
本文为个人学习总结,所有命令均在MySQL* 8.0环境下验证。游标是把“双刃剑”,在使用过程中需平衡它的优缺点,勿滥用!