一、核心概念:游标是什么?
你可以把游标理解为数据库中的"指针/迭代器":
- 普通的
SELECT语句会一次性返回整个结果集(比如SELECT * FROM users会查出所有用户),而游标可以让你逐行读取这个结果集,对每一行数据进行单独的处理(比如判断、计算、更新等)。 - 形象比喻:游标就像你看书时的书签,一次只定位到一行,处理完这一行再移动到下一行,直到遍历完所有行。
游标的核心特点
- 面向单行:一次只处理结果集中的一行数据;
- 可遍历:支持从第一行移动到最后一行(单向遍历,MySQL仅支持向前遍历);
- 常用于存储过程/函数:几乎不会单独使用,通常嵌套在存储过程/函数中处理复杂的行级逻辑。
二、为什么需要游标?
当你需要对查询结果进行逐行的个性化处理 ,而普通SQL语句(如UPDATE、INSERT)无法满足时,游标就派上用场了。比如:
- 遍历订单表,对每个用户的订单金额超过1000的订单自动打9折;
- 统计每个部门的员工薪资,对薪资低于平均值的员工生成提醒;
- 批量处理数据时,逐行校验数据合法性,不合法的记录单独记录日志。
三、游标使用步骤(MySQL实战)
游标使用遵循固定的"五步流程",下面结合一个完整示例(遍历用户表,给年龄<18的用户标记为"未成年")讲解:
完整示例代码
sql
-- 创建存储过程:使用游标标记未成年用户
DELIMITER //
CREATE PROCEDURE MarkMinorUsers()
BEGIN
-- 1. 声明变量:存储游标读取的每行数据
DECLARE user_id INT;
DECLARE user_age INT;
-- 声明结束标志变量(必须是INT,0/1表示是否结束)
DECLARE done INT DEFAULT 0;
-- 2. 声明游标:关联查询结果集
DECLARE user_cursor CURSOR FOR
SELECT id, age FROM users; -- 游标关联"用户ID+年龄"的结果集
-- 3. 声明异常处理:当游标遍历完所有行时,设置done=1
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;
-- 4. 打开游标:执行关联的SELECT语句,加载结果集到内存
OPEN user_cursor;
-- 5. 遍历游标:逐行读取并处理数据
read_loop: LOOP -- 定义循环标签read_loop
-- 读取当前行数据到变量(顺序要和游标SELECT的列一致)
FETCH user_cursor INTO user_id, user_age;
-- 判断是否遍历完成,完成则退出循环
IF done = 1 THEN
LEAVE read_loop;
END IF;
-- 核心业务逻辑:判断年龄,标记未成年
IF user_age < 18 THEN
UPDATE users SET status = '未成年' WHERE id = user_id;
END IF;
END LOOP read_loop;
-- 6. 关闭游标:释放内存
CLOSE user_cursor;
-- 提示执行完成
SELECT '已完成未成年用户标记' AS result;
END //
DELIMITER ;
-- 调用存储过程
CALL MarkMinorUsers();
关键步骤拆解
| 步骤 | 语法/操作 | 作用 |
|---|---|---|
| 1. 声明变量 | DECLARE 变量名 类型; |
存储游标逐行读取的数据(如用户ID、年龄),并声明done变量标记遍历结束 |
| 2. 声明游标 | DECLARE 游标名 CURSOR FOR SELECT语句; |
把游标和一个查询结果集绑定(告诉游标要遍历哪些数据) |
| 3. 异常处理 | DECLARE CONTINUE HANDLER FOR NOT FOUND SET done=1; |
游标遍历到最后一行后,MySQL会抛出"NOT FOUND"异常,这里捕获并设置done=1,避免报错 |
| 4. 打开游标 | OPEN 游标名; |
执行游标绑定的SELECT语句,把结果集加载到内存,准备遍历 |
| 5. 遍历游标 | FETCH 游标名 INTO 变量1, 变量2; |
读取当前行数据到变量,结合循环逐行处理;LEAVE用于退出循环 |
| 6. 关闭游标 | CLOSE 游标名; |
释放游标占用的内存(必须关闭,否则会造成资源泄漏) |
四、使用游标注意事项(新手避坑)
- 性能问题 :游标是逐行处理数据,效率远低于批量SQL操作(如
UPDATE users SET status='未成年' WHERE age<18),能不用就不用,仅在批量操作无法满足复杂逻辑时使用。 - 资源占用 :打开游标后会占用数据库连接和内存,必须在遍历完成后
CLOSE,否则会导致连接泄漏。 - 只读特性 :MySQL的游标默认是"只读"的,无法通过游标直接修改当前行的数据(需通过
UPDATE语句单独修改)。 - 单向遍历:MySQL仅支持"向前遍历",不能回退到上一行,也不能跳行遍历。
- 事务影响 :游标遍历过程中如果执行
UPDATE/DELETE,会影响事务,建议在存储过程中明确控制事务(START TRANSACTION/COMMIT/ROLLBACK)。
五、游标 vs 批量SQL(新手怎么选?)
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 简单条件的批量更新/删除 | 批量SQL(如UPDATE ... WHERE ...) |
效率高,语法简单 |
| 逐行复杂判断(如多表关联校验、动态计算) | 游标 | 能处理单行的个性化逻辑 |
| 大数据量(10万+行) | 避免游标 | 游标逐行处理会导致数据库卡顿,建议分批处理或用ETL工具 |
总结
- 游标本质:数据库中的"行级迭代器",用于逐行读取和处理查询结果集,通常嵌套在存储过程中使用。
- 核心流程:声明变量→声明游标→声明异常处理→打开游标→遍历处理→关闭游标(六步必记)。
- 使用原则:优先用批量SQL,仅在复杂行级逻辑无法用批量操作实现时才用游标,且注意及时关闭释放资源。
游标(Cursor) 和循环(Loop) 的核心区别
游标(Cursor) 和循环(Loop) 的核心区别,这是新手很容易混淆的两个概念------简单来说,游标是"遍历数据的工具",而循环是"重复执行代码的逻辑结构",二者常配合使用,但本质完全不同。
一、核心区别:先看本质
| 维度 | 游标 (Cursor) | 循环 (Loop) |
|---|---|---|
| 本质 | 数据库专属的数据遍历工具(指针) | 通用的代码重复执行逻辑(流程控制) |
| 核心作用 | 逐行读取查询结果集中的数据(定位到某一行) | 让一段代码重复执行N次,或直到满足条件为止 |
| 依赖关系 | 必须基于SELECT结果集,无法独立遍历"空" |
可以独立运行(比如循环打印1-10),不依赖数据 |
| 适用范围 | 仅数据库(存储过程/函数中) | 所有编程语言(Python/Java/MySQL等)都有 |
| 是否可独立使用 | 不能(必须嵌套在循环里才能遍历完所有行) | 能(比如LOOP+LEAVE实现简单循环) |
形象比喻
- 游标:像你手里的"购物清单",清单上列着所有要处理的商品(对应数据库结果集),游标帮你"指到当前要处理的那一项";
- 循环:像你"重复拿清单→处理商品→看下一项"的动作,直到清单全部处理完。
二、单独使用示例:看清各自的作用
1. 仅用Loop(无游标):纯逻辑循环
比如在MySQL存储过程中,用Loop循环打印1-5(无任何数据遍历):
sql
DELIMITER //
CREATE PROCEDURE SimpleLoopDemo()
BEGIN
DECLARE num INT DEFAULT 1; -- 初始化变量
my_loop: LOOP
-- 打印当前数字
SELECT num AS current_number;
-- 数字+1
SET num = num + 1;
-- 条件满足则退出循环
IF num > 5 THEN
LEAVE my_loop;
END IF;
END LOOP my_loop;
END //
DELIMITER ;
CALL SimpleLoopDemo(); -- 输出1、2、3、4、5
这个例子里没有游标,Loop只是单纯重复执行"打印+数字+1"的逻辑,和数据库表数据无关。
2. 游标必须配合Loop:否则只能读1行
如果只开游标、不写循环,游标只能读取结果集的第一行,无法遍历所有行:
sql
DELIMITER //
CREATE PROCEDURE CursorWithoutLoop()
BEGIN
DECLARE user_name VARCHAR(50);
-- 声明游标(关联用户表)
DECLARE name_cursor CURSOR FOR SELECT name FROM users LIMIT 3;
OPEN name_cursor;
-- 仅读取1行
FETCH name_cursor INTO user_name;
SELECT user_name; -- 仅输出第一个用户名
CLOSE name_cursor;
END //
DELIMITER ;
CALL CursorWithoutLoop(); -- 只能拿到第一行数据
这个例子说明:游标本身只能"定位一行",要遍历所有行,必须靠Loop重复执行FETCH。
三、经典配合场景:游标+Loop遍历所有数据
这是最常见的用法------Loop提供"重复执行"的逻辑,游标提供"逐行取数据"的能力,结合起来完成批量行处理:
sql
DELIMITER //
CREATE PROCEDURE CursorWithLoop()
BEGIN
DECLARE done INT DEFAULT 0;
DECLARE user_name VARCHAR(50);
-- 1. 声明游标(绑定数据)
DECLARE name_cursor CURSOR FOR SELECT name FROM users LIMIT 3;
-- 2. 异常处理
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;
OPEN name_cursor;
-- 3. Loop提供循环逻辑
read_loop: LOOP
-- 游标逐行取数据
FETCH name_cursor INTO user_name;
-- 循环终止条件(游标遍历完则退出)
IF done = 1 THEN
LEAVE read_loop;
END IF;
-- 处理每行数据(这里仅打印)
SELECT CONCAT('当前处理用户:', user_name) AS result;
END LOOP read_loop;
CLOSE name_cursor;
END //
DELIMITER ;
CALL CursorWithLoop(); -- 遍历输出3个用户名
关键分工
- Loop:负责"重复执行→判断是否退出",是"循环的骨架";
- 游标:负责"每次循环时取一行数据",是"循环的数据源";
- 二者缺一不可:没有Loop,游标只能取1行;没有游标,Loop只是空转(无数据可处理)。
四、补充:MySQL中的其他循环(不止LOOP)
MySQL里除了LOOP,还有WHILE、REPEAT循环,都可以和游标配合,本质逻辑一致:
sql
-- 示例:WHILE循环+游标
DELIMITER //
CREATE PROCEDURE CursorWithWhile()
BEGIN
DECLARE done INT DEFAULT 0;
DECLARE user_age INT;
DECLARE age_cursor CURSOR FOR SELECT age FROM users;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;
OPEN age_cursor;
-- WHILE循环
WHILE done = 0 DO
FETCH age_cursor INTO user_age;
IF done = 0 THEN
SELECT user_age;
END IF;
END WHILE;
CLOSE age_cursor;
END //
DELIMITER ;
不管用哪种循环,核心都是:循环提供"重复执行"的逻辑,游标提供"逐行取数据"的能力。
总结
- 核心区别:游标是"数据遍历工具"(针对数据库结果集),循环是"代码重复执行逻辑"(通用流程控制);
- 配合关系:游标必须依赖循环才能遍历完所有行,循环可以脱离游标独立执行(如纯数字循环);
- 使用场景:当需要逐行处理数据库结果集时,用「游标+循环」组合;仅需重复执行代码(无数据遍历)时,只用循环即可。