CategoryResourceRepost/极客时间专栏/SQL必知必会/第一章:SQL语法基础篇/16丨游标:当我们需要逐条处理数据时,该怎么做?.md
louzefeng d3828a7aee mod
2024-07-11 05:50:32 +00:00

278 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<audio id="audio" title="16丨游标当我们需要逐条处理数据时该怎么做" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/da/a7/da00a389262c0d9738859a04b53045a7.mp3"></audio>
我们在编写SQL语句的时候通常是面向集合进行思考这种思考方式更让我们关注结果集的特征而不是具体的实现过程。面向集合的思考方式与面向过程的思考方式各有特点我们该如何理解它们呢
我们用下面这张图开启今天的学习。这张图中一共有9个图形每个图形有不同的特征包括形状、纹理、颜色和个数等。
<img src="https://static001.geekbang.org/resource/image/3d/8b/3daf9a9168ac825e9e7943843175bb8b.jpg" alt=""><br>
当我们看到这张图时有时候会不由自主地按照某个属性进行分类比如说按照红色分类那么1、4、9就是一类。这实际上就是属于同一个条件下的查询结果集。或者我们也可以按照物体的个数来划分比如都有3个物体的那么对应的就是2、5、6、8这就是对应着“都包括3个物体”的查询结果集。
你能看出来集合思维更像是从整体的角度来考虑然后把整个数据集按照不同的属性进行划分形成不同的子集合。面向集合的思考方式让我们关注“获取什么”而不是“如何获取”这也可以说是SQL与传统编程最大的区别之一因为SQL本身是以关系模型和集合论为基础的。
然而也有一些情况,我们不需要对查询结果集中的所有数据行都采用相同的处理方式,需要每次处理一行或者一部分行,这时就需要面向过程的编程方法了。游标就是这种编程方式的体现。如果你之前已经有了一些面向过程的编程经验,那么对于游标的理解也会比较容易。
关于游标,你需要掌握以下几个方面的内容:
1. 什么是游标?我们为什么要使用游标?
1. 如何使用游标?使用游标的常用步骤都包括哪些?
1. 如何使用游标来解决一些常见的问题?
## 什么是游标?
在数据库中游标是个重要的概念它提供了一种灵活的操作方式可以让我们从数据结果集中每次提取一条数据记录进行操作。游标让SQL这种面向集合的语言有了面向过程开发的能力。可以说游标是面向过程的编程方式这与面向集合的编程方式有所不同。
在SQL中游标是一种临时的数据库对象可以指向存储在数据库表中的数据行指针。这里游标充当了指针的作用我们可以通过操作游标来对数据行进行操作。
比如我们查询了heros数据表中最大生命值大于8500的英雄都有哪些
```
SELECT id, name, hp_max FROM heros WHERE hp_max &gt; 8500
```
查询结果4条数据
<img src="https://static001.geekbang.org/resource/image/04/c0/046f997b7d1b2ce6a64e65b728cca4c0.jpg" alt="">
这里我们就可以通过游标来操作数据行,如图所示此时游标所在的行是“白起”的记录,我们也可以在结果集上滚动游标,指向结果集中的任意一行。
## 如何使用游标?
游标实际上是一种控制数据集的更加灵活的处理方式。
如果我们想要使用游标一般需要经历五个步骤。不同DBMS中使用游标的语法可能略有不同。
第一步,定义游标。
```
DECLARE cursor_name CURSOR FOR select_statement
```
这个语法适用于MySQLSQL ServerDB2和MariaDB。如果是用Oracle或者PostgreSQL需要写成
```
DECLARE cursor_name CURSOR IS select_statement
```
要使用SELECT语句来获取数据结果集而此时还没有开始遍历数据这里select_statement代表的是SELECT语句。
下面我用MySQL举例讲解游标的使用如果你使用的是其他的RDBMS具体的游标语法可能略有差异。我们定义一个能够存储heros数据表中的最大生命值的游标可以写为
```
DECLARE cur_hero CURSOR FOR
SELECT hp_max FROM heros;
```
第二步,打开游标。
```
OPEN cursor_name
```
当我们定义好游标之后如果想要使用游标必须先打开游标。打开游标的时候SELECT语句的查询结果集就会送到游标工作区。
第三步,从游标中取得数据。
```
FETCH cursor_name INTO var_name ...
```
这句的作用是使用cursor_name这个游标来读取当前行并且将数据保存到var_name这个变量中游标指针指到下一行。如果游标读取的数据行有多个列名则在INTO关键字后面赋值给多个变量名即可。
第四步,关闭游标。
```
CLOSE cursor_name
```
有OPEN就会有CLOSE也就是打开和关闭游标。当我们使用完游标后需要关闭掉该游标。关闭游标之后我们就不能再检索查询结果中的数据行如果需要检索只能再次打开游标。
最后一步,释放游标。
```
DEALLOCATE cursor_namec
```
有DECLARE就需要有DEALLOCATEDEALLOCATE的作用是释放游标。我们一定要养成释放游标的习惯否则游标会一直存在于内存中直到进程结束后才会自动释放。当你不需要使用游标的时候释放游标可以减少资源浪费。
上面就是5个常用的游标步骤。我来举一个简单的例子假设我想用游标来扫描heros数据表中的数据行然后累计最大生命值那么该怎么做呢
我先创建一个存储过程calc_hp_max然后在存储过程中定义游标cur_hero使用FETCH获取每一行的具体数值然后赋值给变量hp再用变量hp_sum做累加求和最后再输出hp_sum代码如下
```
CREATE PROCEDURE `calc_hp_max`()
BEGIN
-- 创建接收游标的变量
DECLARE hp INT;
-- 创建总数变量
DECLARE hp_sum INT DEFAULT 0;
-- 创建结束标志变量
DECLARE done INT DEFAULT false;
-- 定义游标
DECLARE cur_hero CURSOR FOR SELECT hp_max FROM heros;
OPEN cur_hero;
read_loop:LOOP
FETCH cur_hero INTO hp;
SET hp_sum = hp_sum + hp;
END LOOP;
CLOSE cur_hero;
SELECT hp_sum;
END
```
你会发现执行`call calc_hp_max()`这一句的时候系统会提示1329错误也就是在LOOP中当游标没有取到数据时会报的错误。
当游标溢出时也就是当游标指向到最后一行数据后继续执行会报的错误我们可以定义一个continue的事件指定这个事件发生时修改变量done的值以此来判断游标是否已经溢出
```
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = true;
```
同时在循环中我们需要加上对done的判断如果游标的循环已经结束就需要跳出read_loop循环完善的代码如下
```
CREATE PROCEDURE `calc_hp_max`()
BEGIN
-- 创建接收游标的变量
DECLARE hp INT;
-- 创建总数变量
DECLARE hp_sum INT DEFAULT 0;
-- 创建结束标志变量
DECLARE done INT DEFAULT false;
-- 定义游标
DECLARE cur_hero CURSOR FOR SELECT hp_max FROM heros;
-- 指定游标循环结束时的返回值
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = true;
OPEN cur_hero;
read_loop:LOOP
FETCH cur_hero INTO hp;
-- 判断游标的循环是否结束
IF done THEN
LEAVE read_loop;
END IF;
SET hp_sum = hp_sum + hp;
END LOOP;
CLOSE cur_hero;
SELECT hp_sum;
END
```
运行结果1行数据
<img src="https://static001.geekbang.org/resource/image/25/74/250fa19076fb9e55f801847815eb8674.png" alt="">
在游标中的循环中除了使用LOOP循环以外你还可以使用REPEAT… UNTIL…以及WHILE循环。它们同样需要设置CONTINUE事件来处理游标溢出的情况。
所以你能看出使用游标可以让我们对SELECT结果集中的每一行数据进行相同或者不同的操作从而很精细化地管理结果集中的每一条数据。
**使用游标来解决一些常见的问题**
我刚才讲了一个简单的使用案例实际上如果想要统计hp_sum完全可以通过SQL语句来完成比如
```
SELECT SUM(hp_max) FROM heros
```
运行结果1行数据
<img src="https://static001.geekbang.org/resource/image/3b/9f/3b26582a9d86399c7b8c240e82369f9f.png" alt=""><br>
那么游标都有什么用呢?
当你需要处理一些复杂的数据行计算的时候游标就会起到作用了。我举个例子还是针对heros数据表假设我们想要对英雄的物攻成长对应attack_growth进行升级在新版本中大范围提升英雄的物攻成长数值但是针对不同的英雄情况提升的幅度也不同具体提升的方式如下。
如果这个英雄原有的物攻成长小于5那么将在原有基础上提升7%-10%。如果物攻成长的提升空间即最高物攻attack_max-初始物攻attack_start大于200那么在原有的基础上提升10%如果物攻成长的提升空间在150到200之间则提升8%如果物攻成长的提升空间不足150则提升7%。
如果原有英雄的物攻成长在5—10之间那么将在原有基础上提升5%。
如果原有英雄的物攻成长大于10则保持不变。
以上所有的更新后的物攻成长数值都需要保留小数点后3位。
你能看到上面这个计算的情况相对复杂,实际工作中你可能会遇到比这个更加复杂的情况,这时你可以采用面向过程的思考方式来完成这种任务,也就是说先取出每行的数值,然后针对数值的不同情况采取不同的计算方式。
针对上面这个情况,你自己可以用游标来完成转换,具体的代码如下:
```
CREATE PROCEDURE `alter_attack_growth`()
BEGIN
-- 创建接收游标的变量
DECLARE temp_id INT;
DECLARE temp_growth, temp_max, temp_start, temp_diff FLOAT;
-- 创建结束标志变量
DECLARE done INT DEFAULT false;
-- 定义游标
DECLARE cur_hero CURSOR FOR SELECT id, attack_growth, attack_max, attack_start FROM heros;
-- 指定游标循环结束时的返回值
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = true;
OPEN cur_hero;
FETCH cur_hero INTO temp_id, temp_growth, temp_max, temp_start;
REPEAT
IF NOT done THEN
SET temp_diff = temp_max - temp_start;
IF temp_growth &lt; 5 THEN
IF temp_diff &gt; 200 THEN
SET temp_growth = temp_growth * 1.1;
ELSEIF temp_diff &gt;= 150 AND temp_diff &lt;=200 THEN
SET temp_growth = temp_growth * 1.08;
ELSEIF temp_diff &lt; 150 THEN
SET temp_growth = temp_growth * 1.07;
END IF;
ELSEIF temp_growth &gt;=5 AND temp_growth &lt;=10 THEN
SET temp_growth = temp_growth * 1.05;
END IF;
UPDATE heros SET attack_growth = ROUND(temp_growth,3) WHERE id = temp_id;
END IF;
FETCH cur_hero INTO temp_id, temp_growth, temp_max, temp_start;
UNTIL done = true END REPEAT;
CLOSE cur_hero;
END
```
这里我创建了alter_attack_growth这个存储过程使用了REPEAT…UNTIL…的循环方式针对不同的情况计算了新的物攻成长temp_growth然后对原有的attack_growth进行了更新最后调用call alter_attack_growth();执行存储过程。
有一点需要注意的是我们在对数据表进行更新前需要备份之前的表我们可以将备份后的表命名为heros_copy1。更新完heros数据表之后你可以看下两张表在attack_growth字段上的对比我们使用SQL进行查询
```
SELECT heros.id, heros.attack_growth, heros_copy1.attack_growth FROM heros JOIN heros_copy1 WHERE heros.id = heros_copy1.id
```
运行结果69条记录
<img src="https://static001.geekbang.org/resource/image/62/c9/622c50afb8ff6be7d9682fe3537dedc9.png" alt=""><br>
通过前后两张表的attack_growth对比你也能看出来存储过程通过游标对不同的数据行进行了更新。
需要说明的是以上代码适用于MySQL如果在SQL Server或Oracle中使用方式会有些差别。
## 总结
今天我们讲解了如何在SQL中使用游标游标实际上是面向过程的思维方式与面向集合的思维方式不同的地方在于游标更加关注“如何执行”。我们可以通过游标更加精细、灵活地查询和管理想要的数据行。
有的时候我们需要找特定数据用SQL查询写起来会比较困难比如两表或多表之间的嵌套循环查找如果用JOIN会非常消耗资源效率也可能不高而用游标则会比较高效。
虽然在处理某些复杂的数据情况下,使用游标可以更灵活,但同时也会带来一些性能问题,比如在使用游标的过程中,会对数据行进行加锁,这样在业务并发量大的时候,不仅会影响业务之间的效率,还会消耗系统资源,造成内存不足,这是因为游标是在内存中进行的处理。如果有游标的替代方案,我们可以采用替代方案。
<img src="https://static001.geekbang.org/resource/image/dc/11/dca1fadf6625b9699c25104e74fb8d11.jpg" alt=""><br>
我们今天讲解了游标你能用自己的语言介绍下游标的作用吗另外我们之前提到过SQL本身是一门结构化查询语言但我们也可以在SQL的基础上进行面向过程的开发完成较为复杂的功能你能说一下面向过程和面向集合这两种编程方式的区别吗
欢迎你在评论区写下你的思考,也欢迎把这篇文章分享给你的朋友或者同事,一起交流一下。