🛢️ 第7章 MySQL基础
📄 一句话概括

IF 和 CASE 是表达式,能塞进 SELECT;循环只能写在存储过程里。 CASE 有两种形态:比等值的简单形态、比条件的搜索形态,不能混用。 三种循环的差别只有一个:什么时候判断条件。先判断、后判断、不判断。

📝 本章和前面几章的关系

7.1 讲的是「怎么算」,这一篇讲「按条件走哪条路」。 它是第 18 章存储过程的前置:循环必须写在存储过程里,这一篇先讲语义。 这一篇的三值逻辑会再出现一次:CASE WHEN NULL 不会命中,会落到 ELSE。

一、IF 是表达式,不是语句

MySQL 的 IF(条件, 真值, 假值) 长得像函数,本质是一个表达式。 表达式意味着它能出现在任何「值能出现的地方」,包括 SELECT 的列表。

SELECT name, price, IF(price > 100, '贵', '便宜') AS level FROM tb_goods;
UPDATE tb_goods SET level = IF(price > 100, '贵', '便宜');

这两句说明它的用处:按条件给一列打标签。 它和编程语言里的 if 语句不是一回事——那个是控制流程,只影响「走哪条路」,不产生值。 区分清楚这一点,后面 CASE 就好懂了:CASE 也是表达式,也是产生一个值。 所以它们能出现在 SELECT、WHERE、ORDER BY 里,但不能用来「跳过某条语句」。

二、CASE 的两种形态

CASE 有两种写法,教材 7.2 都讲了,区别在于「比什么」。

-- 简单 CASE:把一个列拿去和几个值比
SELECT CASE sex WHEN 'M' THEN '男' WHEN 'F' THEN '女' ELSE '未知' END FROM tb;

-- 搜索 CASE:每个 WHEN 后面是完整条件
SELECT CASE WHEN score >= 90 THEN '优' WHEN score >= 60 THEN '及格' END FROM tb;

第一种只在「同一个列比几个固定值」时好用,第二种什么条件都能写。 实践里第二种用得多,因为它能比区间、能组合多个列。 两种形态不能混用。 写了 CASE 列 WHEN 就只能 WHEN 值,写成 WHEN 条件 不一定报错,但结果会很怪。 这属于「语法上过得去、语义上不对」的坑,比报错更难查。 还有一条顺序规则:CASE 是按顺序匹配的,第一个成立的就返回,后面的不再看。 所以 WHEN score >= 60 必须写在 WHEN score >= 90 后面,反了就全变「及格」。

三、CASE 与 NULL 撞在一起

这一节教材没讲,但它一定会撞上。 CASE WHEN 列 >= 60 在列是 NULL 时,条件算出来是 NULL,不算成立。 所以缺考的学生会被分到 ELSE 那一支,变成「不及格」。 可「缺考」和「考零分」应该是两种状态,用这套 CASE 分不出来。

CASE WHEN score IS NULL THEN '缺考'
     WHEN score >= 90   THEN '优'
     WHEN score >= 60   THEN '及格'
     ELSE '不及格' END

把 IS NULL 那一支放最前面,就能分开。 注意它必须放最前面——放后面就轮不到它了,因为前面的 ELSE 已经兜住了。 这条经验可以推广:凡是 CASE 里要处理空值,就把空值那支写在第一个。 它和「WHERE 里判断空要用 IS NULL」是同一条原理的两种表现。

四、三种循环:差别只在什么时候判断

教材 7.2 讲了 WHILE、LOOP、REPEAT 三种。名字不同,功能几乎一样。 真正的差别只有一处:条件在什么时候判断。

循环 什么时候判断 最少跑几次 出口怎么写
WHILE 执行之前 0 次 条件写在开头
REPEAT 执行之后 1 次 UNTIL 条件 写在末尾
LOOP 不判断 0 次也能进去 必须自己 LEAVE

第二列是这一节的全部内容。 WHILE 条件不成立就一次都不跑;REPEAT 至少跑一次,因为它是跑完才判断。 这个差别在「条件一开始就不成立」时最明显。 比如「从 100 加到 10」,WHILE 给 0,REPEAT 给 100——后者多做了一次不该做的事。 LOOP 那一行值得单独说:它自己不带任何条件,所以写成死循环是常态。 必须自己 LEAVE 才能出来。 它的用处是「出口条件很复杂、一句话写不出来」的时候。

五、LEAVE 与 ITERATE:两个陌生的名字

这两个名字第一次见会懵,其实就是 break 和 continue。

关键字 相当于 作用
LEAVE 标签 break 跳出整个循环
ITERATE 标签 continue 跳过本次,回去继续下一次

两个都要跟一个「标签」,所以循环得起个名字。 标签的写法是 名字: LOOP ... END LOOP 名字;,冒号在前,名字在 END 后也要写一遍。 ITERATE 比 break 更容易用错。 它「跳过本次」的意思是「本次剩下的语句不执行了, 直接进入下一次判断」——所以写在它前面的累加不会被执行,写在它后面的才有机会。

六、循环只能进存储过程

这一节是教材没强调、但一定撞得上的一条。 WHILE、REPEAT、LOOP 不能出现在普通 SQL 里。 它们只能写在存储过程、函数、触发器、事件这四类对象内部。 所以「用 SQL 写个循环」的前提是先建一个存储过程。 很多人第一次写循环就报语法错,就是因为把它写在了客户端里。 但更该记住的是下一句:能用一条 SQL 解决的,就别写循环。 SQL 是按集合算的,循环是逐行算的。 同样一件事,集合写法可能比循环快几十倍,因为前者一次读写一批页,后者每行都要走一趟。 判断标准很简单:如果循环体里每条语句只处理一行, 那它多半能被一条 UPDATE 或 INSERT ... SELECT 替掉。 真需要循环的场景很少:分批处理大表、按序做有依赖的计算、生成测试数据。

七、动手 Demo:IF、CASE、三种循环,一条一条跑

这一课把 IF 和 CASE 真跑一遍,再用 Python 按语义复刻三种循环。 循环的 MySQL 原码会贴出来对照——沙箱里它们跑不了,但语义是确定的。 其中 CASE 那一段会出现一个真问题:缺考的学生被分成了「不及格」。

# 一、IF 是表达式
c0.execute("SELECT iif(?, '真', '假')", (1,))     # SQLite 把它叫 iif

# 二、CASE 分级:注意 NULL 落到 ELSE
c.execute("SELECT CASE WHEN ? >= 90 THEN '优' "
          "WHEN ? >= 80 THEN '良' "
          "WHEN ? >= 60 THEN '及格' ELSE '不及格' END", (score, score, score))

# 三、三种循环的语义复刻
def while_sum(start, end):
    s, i = 0, start
    while i <= end:
        s += i; i += 1
    return s

def repeat_sum(start, end):
    s, i = 0, start
    while True:
        s += i; i += 1
        if i > end: break        # UNTIL i > end
    return s

# 四、LEAVE 与 ITERATE
if i > 5: break                   # LEAVE
if i % 2 == 0: continue           # ITERATE

实跑输出:

--------------------------------------------------------------------
第三十二课:IF 和 CASE 是表达式,循环只能进存储过程
--------------------------------------------------------------------

  **教材 7.2 把流程控制分成两支:`IF`/`CASE` 是「选一个」,三种循环是「重复做」。**
  **这一支分得很关键:前者能塞进 `SELECT`,后者只能写在存储过程里。**
  **这一课两条都走一遍。能真跑的跑,跑不了的按语义复刻。**

  一、IF 是表达式,不是语句
  --------------------------------------------------------------------
  **MySQL 的 `IF(条件, 真值, 假值)` 是表达式,可以出现在 `SELECT` 的列里。**
  **这和编程语言里的 `if` 语句不是一回事。**
  **SQLite 对应的是 `iif()`,三个参数位置一样。**

  表达式              本次实测  MySQL 写法         MySQL 结果
  -----------------------------------------------------------
  iif(1, '真', '假')  真        IF(1, '真', '假')  真
  iif(0, '真', '假')  假        IF(0, '真', '假')  假
  **两个结果都和 MySQL 一致,因为「条件为真取第一个值」这件事没有歧义。**
  **真正要注意的是它的位置:它出现在 `SELECT` 的列表里,说明它是表达式,能参与运算。**
  **所以可以写 `SELECT name, IF(price > 100, '贵', '便宜') FROM tb`。**
  **这种「按条件给一列打标签」的活,用它最省事。**

  二、CASE 的两种形态
  --------------------------------------------------------------------

  [教材原码 · 本沙箱不执行]
  | SELECT CASE sex WHEN 'M' THEN '男' WHEN 'F' THEN '女' ELSE '未知' END FROM tb;
  | SELECT CASE WHEN score >= 90 THEN '优' WHEN score >= 60 THEN '及格' END FROM tb;
  ② 教材原码,本沙箱不执行

  **第一种叫「简单 CASE」,它比的是「等不等」。**
  **第二种叫「搜索 CASE」,每个 `WHEN` 后面是一个完整条件,比分区间用它。**
  **两种形态不能混用:写了 `CASE 列 WHEN` 就只能比等值,要写条件必须把列去掉。**
  **混着写不一定报错,但结果会很怪。**
  **五个人,分数分别是 95、82、61、45 和「没考」。真跑一遍分级:**

  姓名  分数  CASE 给的结果
  -------------------------
  赵一  95    优
  钱二  82    良
  孙三  61    及格
  李四  45    不及格
  周五  NULL  不及格
  **第五个人没考,分数是 NULL,落到 `ELSE` 那一支,成了「不及格」。**
  **这是个真问题:缺考和考零分应该是两种状态,用 CASE 分不出来。**
  **原因是 `NULL >= 60` 给的是 NULL,不是假,所以每个 `WHEN` 都不成立。**
  **要区分缺考,得在最前面加一句 `WHEN score IS NULL THEN '缺考'`。**
  **这一条是 CASE 与三值逻辑撞在一起的地方,很值得记住。**

  三、WHILE、REPEAT、LOOP:三种循环
  --------------------------------------------------------------------

  [教材原码 · 本沙箱不执行]
  | -- WHILE:先判断后执行,可能一次都不跑
  | WHILE i <= 10 DO
  |     SET s = s + i;  SET i = i + 1;
  | END WHILE;
  | 
  | -- REPEAT:先执行后判断,至少跑一次
  | REPEAT
  |     SET s = s + i;  SET i = i + 1;
  | UNTIL i > 10 END REPEAT;
  | 
  | -- LOOP:没有出口,必须自己 LEAVE
  | my_loop: LOOP
  |     SET s = s + i;  SET i = i + 1;
  |     IF i > 10 THEN LEAVE my_loop; END IF;
  | END LOOP my_loop;
  ③ 教材原码,本沙箱不执行

  **三种循环的差别不在语法,在「什么时候判断条件」。**
  **`WHILE` 先判断,`REPEAT` 后判断,`LOOP` 不判断、全靠你自己 `LEAVE`。**

  写法    1 加到 10  起始值 100(条件一开始就不成立)
  ---------------------------------------------------
  WHILE   55         0
  REPEAT  55         100
  LOOP    55         100
  **第一列三种都是 55,说明三种写法能表达同一件事。**
  **第二列才是差别:`WHILE` 给 0(一次没跑),`REPEAT` 和 `LOOP` 都给 100(都跑了一次)。**
  **这就是「先判断」和「后判断」的全部区别。**
  **实践中 `WHILE` 用得多,`REPEAT` 只在「必须先做一次」时用,`LOOP` 几乎不用。**

  四、LEAVE 和 ITERATE:break 与 continue
  --------------------------------------------------------------------

  [教材原码 · 本沙箱不执行]
  | -- LEAVE 相当于 break:跳出整个循环
  | IF i > 5 THEN LEAVE my_loop; END IF;
  | 
  | -- ITERATE 相当于 continue:跳过本次,回去继续
  | IF MOD(i, 2) = 0 THEN ITERATE my_loop; END IF;
  ④ 教材原码,本沙箱不执行

  **两个名字很陌生,功能一点都不陌生:`LEAVE` 是 `break`,`ITERATE` 是 `continue`。**
  **它们后面都要跟一个标签名,所以循环得先起个名字。**

  写法                        1 加到 10 的结果
  --------------------------------------------
  正常跑完                    55
  i > 5 就 LEAVE(break)     15
  偶数就 ITERATE(continue)  25
  **第二行是 15,正好是 1 到 5 的和,说明从 6 开始就跳出去了。**
  **第三行是 25,正好是 1、3、5、7、9 的和,说明偶数被跳过了。**
  **这两个数是可验算的,建议自己心算一遍。**
  **循环写错的时候,往往就是这两处出了问题:该跳出的没跳出,该跳过的没跳过。**

  五、循环只能写在存储过程里
  --------------------------------------------------------------------
  **这一节是教材没强调、但一定会撞上的一条。**
  **`WHILE`、`REPEAT`、`LOOP` 不能出现在普通的 `SELECT` 或 `INSERT` 里。**
  **它们只能写在存储过程、函数、触发器、事件这四类对象内部。**
  **所以「用 SQL 写个循环」这件事,前提是你得先建一个存储过程。**
  **这也是很多人第一次写循环就报语法错的原因——他们把它写在了客户端里。**
  **反过来说,能用一条 SQL 解决的事,就别写循环。**
  **SQL 是按集合算的,循环是逐行算的,后者往往慢一个数量级。**
  **这一点在第 18 章讲存储过程时会再展开。**

第一段两行都和 MySQL 一致。iif() 是 SQLite 的写法,IF() 是 MySQL 的,三个参数位置一样。 要紧的是它在 SELECT 列表里——这说明它是表达式,不是语句。 第二段那张分级表,前四个都正常,第五个出了问题:周五 的分数是 NULL,结果是「不及格」。 因为 NULL >= 90 给的是 NULL,不是假,所以每个 WHEN 都不成立,最后落到 ELSE。 这就是三值逻辑进 CASE 的样子。 修法是在最前面加一支 WHEN score IS NULL THEN '缺考'。 必须放最前面,放后面就轮不到它。 第三段那张表:WHILE 和 REPEAT 在「1 加到 10」时都给 55,看不出区别。 换成「从 100 加到 10」,WHILE 给 0,REPEAT 给 100——差别全出来了。 第四段两个数是 15 和 25。 15 是 1 到 5 的和(6 开始就 LEAVE 了),25 是 1、3、5、7、 9 的和(偶数都 ITERATE 掉了)。 这两个数可以手算验证,建议验一遍。

30 秒实验

  1. 把 CASE 里 >= 90 和 >= 60 这两支的顺序对调,再跑一次。95 分的人会变成「及格」。 因为 CASE 按顺序匹配,第一个成立就返回,后面的不再看。
  2. 给 WHILE 那一段加上 i = 0 时 ITERATE,看看会不会死循环。 会。因为 i 没机会加一,条件永远成立。 这是 continue 最经典的坑,写循环时要先确认「自增那一步不会被跳过」。
  3. 想一想:给 10 万行数据按分数打等级,用循环和用一条 UPDATE ... CASE 差多少? 差一个数量级以上。 循环是 10 万次单行更新,语句是 1 次批量更新。

常见误区

  1. 「IF 和 CASE 可以控制流程,跳过某条语句。」 它们都是表达式,只产生一个值。 跳过语句是「如果……就不执行」的意思,SQL 里没有这种东西,只有 WHERE 筛掉行。
  2. 「WHILE 和 REPEAT 是一样的,随便用哪个。」 条件一开始就不成立时,WHILE 一次不跑,REPEAT 跑一次。 这处差别在「必须至少做一次」的场景里是致命的。
  3. 「循环写在 SQL 里就行,反正语法支持。」 循环只能写在存储过程、函数、触发器、事件里。 普通 SELECT 里写 WHILE 直接语法错。
  4. 「CASE 里 ELSE 可以不写。」 不写就是给 NULL。 如果下游拿这一列去比较,NULL 会把整个条件算成「不知道」,又绕回三值逻辑那个坑。
自测三问(点开对答案) 1. `WHILE` 和 `REPEAT` 最本质的差别是什么? —— **条件判断的位置:`WHILE` 在执行前,`REPEAT` 在执行后。** **所以条件一开始不成立时,`WHILE` 跑 0 次,`REPEAT` 跑 1 次。** 2. `LEAVE` 和 `ITERATE` 相当于什么? —— **`LEAVE` 是 `break`(跳出整个循环),`ITERATE` 是 `continue`(跳过本次)。** **两者后面都要跟循环的标签名。** 3. 为什么循环只能写在存储过程里? —— **因为 SQL 是面向集合的语句,一条语句处理一批行,本身没有「逐行控制」的概念。** **循环需要过程化的执行环境,所以只能在存储过程这类对象里。**

第 7 章到这儿结束。下一篇进第 8 章,讲表数据的增、删、改—— 语句会算了,接下来该真往里写数据,INSERT、UPDATE、DELETE 三条一起讲。

相关笔记