🪟 第6章 SQL基础
📄 一句话概括

IF...ELSE 只能跟一条语句;跟多条必须用 BEGIN...END,漏了不报错。 CASE 是唯一的标准流程控制,四家都认;它是表达式不是语句,能写在 SELECT 里。 WHILE 是 T-SQL 的过程化扩展;能用一条 SQL 解决的,别写循环——慢一个量级。

📝 本章和前面几章的关系

上一节(6.4)只讲了语句块和运算符,真正的分支和循环全在这一节。教材 6.5 有七个小节: BEGIN...END、IF...ELSE、CASE、WHILE、WHILE...CONTINUE...BREAK、RETURN、GOTO。 七个里只有 CASE 能真跑——它是标准 SQL。 其余六个是 T-SQL 的过程化扩展,本沙箱跑不了, 演示里用 Python 复刻语义并说清「复刻的是行为不是语法」。 最该带走的三件事: IF 只能管一条语句、CASE 是从上往下匹配、WHILE 慢所以少用。

一、IF...ELSE:只能管一条语句

IF @score >= 60
    PRINT '及格'
ELSE
    PRINT '不及格'

这个结构本身很简单,难在「只能管一条」。 想让 IF 管住三条语句, 必须用 BEGIN...END 包起来——这一点上一节说过,但它是这一节最常犯的错,值得再说一次。 漏写 BEGIN...END 不报错。 只有第一条语句受 IF 控制,后面的无条件执行。 结果是「有时候对、有时候不对」——这种 bug 可能埋几个月。 还有一条容易忽略的:ELSE 是可选的,但 IF 嵌套超过三层就该重构了。三层 IF 套在一起, 读代码的人要在脑子里维护一个栈——这种逻辑用 CASE 或者查表更清楚。

二、CASE:唯一能真跑的

CASE 有两种写法,这是本节最容易混的地方:

写法 长什么样 什么时候用
简单 CASE CASE 列 WHEN 值 THEN 结果 列等于某几个固定值
搜索 CASE CASE WHEN 条件 THEN 结果 条件不是等值判断
-- 简单 CASE:适合等值判断
CASE sex WHEN 'M' THEN '男' WHEN 'F' THEN '女' ELSE '未知' END

-- 搜索 CASE:适合范围判断
CASE WHEN score >= 90 THEN '优秀'
     WHEN score >= 60 THEN '及格'
     ELSE '不及格' END

关键的一条:CASE 是「从上往下匹配,第一个成立的就返回」。所以条件顺序决定结果—— 把 score >= 60 写在最前面,所有 60 分以上的都会变成「及格」,后面的分支永远走不到。 CASE 是表达式,不是语句。 这一点很重要:它能写在 SELECT 里、ORDER BY 里、WHERE 里, 甚至能当 UPDATE 的赋值——因为它返回一个值。 没有 ELSE 的话,所有条件都不满足时返回 NULL。 这个 NULL 会跟着结果走, 后面再参与计算就变成了 NULL——所以「兜底」那个 ELSE 建议写上。

三、WHILE:能用 SQL 就别用循环

DECLARE @i INT = 1
WHILE @i <= 5
BEGIN
    PRINT @i
    SET @i = @i + 1
END

WHILE 是 T-SQL 的过程化扩展,纯查询语句里没有循环。标准 SQL 的替代品是「递归 CTE」, 用查询表达循环——写法完全不一样。 更要紧的一条是性能。 逐行处理的循环,比一条集合操作慢一个量级——十万行数据,集合操作一秒, 循环可能十分钟。这就是所谓的「RBAR」(row by agonizing row)。 所以判断标准是:能不写循环就不写。 更新一张表用 UPDATE ... WHERE,不要 WHILE 一条条改; 汇总用 GROUP BY,不要循环累加。 真的需要循环的场景很少: 分批处理海量数据(一次一万行,避免长事务)、按顺序执行有依赖的操作、 生成测试数据。除了这些,基本都是集合操作能解决的。

四、CONTINUE、BREAK、RETURN、GOTO

关键字 作用 该不该用
CONTINUE 跳过本次,进入下一轮 可以用,注意死循环
BREAK 跳出整个循环 可以用
RETURN 立即退出,可带返回码 存储过程里常用
GOTO 跳到指定标签 不要用

CONTINUE 的死循环风险: 如果「跳过」的那一轮没有推进循环变量, 条件永远成立——循环就出不来了。 RETURN 在存储过程里可以带一个整数返回码。 0 表示成功, 非 0 是错误码——这个习惯跟 C 语言一样,调用方靠这个数判断成败。 GOTO 是唯一一个「教材列了但建议别用」的。 它让控制流可以往任何方向跳,代码读起来像一团线。 现代语言里基本都废弃了,T-SQL 保留它是为了兼容几十年前的老代码。 顺带说一句:GOTO 在老代码里最常见的用法是「出错跳到统一清理段」。 现在这个场景用 TRY...CATCH 代替——能看到 GOTO 的地方,基本都是没重构过的代码。

五、动手 Demo:IF、CASE、WHILE 与 GOTO

这一段贴 IF 和 WHILE 的原码(本沙箱不执行),真跑 CASE——它是标准 SQL,四家都认, 最后用 Python 复刻一次循环,并列出 CONTINUE、BREAK、RETURN、GOTO 四个关键字的用法。

import sqlite3

c = sqlite3.connect(u":memory:")
c.execute(u"CREATE TABLE tb_score (sno TEXT, score INT)")
c.executemany(u"INSERT INTO tb_score VALUES (?,?)",
              [('S01', 92), ('S02', 78), ('S03', 55), ('S04', 61), ('S05', 100)])

q = u"""SELECT sno, score,
       CASE WHEN score >= 90 THEN '优秀'
            WHEN score >= 80 THEN '良好'
            WHEN score >= 60 THEN '及格'
            ELSE '不及格' END AS grade
       FROM tb_score ORDER BY score DESC"""
for r in c.execute(q):
    print(r)

# 用 Python 复刻 WHILE 的语义(不是语法)
i = 1
while i <= 5:
    print(i)
    i += 1

实跑输出:

--------------------------------------------------------------------
IF、CASE、WHILE 与 GOTO
--------------------------------------------------------------------


  ① IF...ELSE:只能跟一条语句
  --------------------------------------------------------------------

  [教材原码 · 本沙箱不执行]
  | IF @score >= 60
  |     PRINT '及格'
  | ELSE
  |     PRINT '不及格'

  要跟多条语句,必须用 BEGIN...END 包起来。这是 T-SQL 里最容易漏的一处。
  漏了不会报错——只有第一条语句受 IF 控制,后面的无条件执行。
  这种错叫「悬空 else」,查起来很费劲,因为它不报错、结果只是「有时候对不上」。

  ② CASE:唯一能真跑的流程控制
  --------------------------------------------------------------------
  sno  score  等级
  S05  100    优秀
  S01  92     优秀
  S02  78     及格
  S04  61     及格
  S03  55     不及格
  CASE 是标准 SQL,四家都认,所以这一段能真跑。
  注意它是「从上往下匹配,第一个成立的就返回」——所以条件的顺序很关键。
  把 score >= 60 写在最前面,那所有及格以上的都会变成「及格」,后面的分支永远走不到。

  ③ WHILE:SQL Server 有,标准 SQL 没有
  --------------------------------------------------------------------

  [教材原码 · 本沙箱不执行]
  | DECLARE @i INT = 1
  | WHILE @i <= 5
  | BEGIN
  |     PRINT @i
  |     SET @i = @i + 1
  | END

  WHILE 是 T-SQL 的过程化扩展,纯查询语句里没有循环。
  标准 SQL 的替代品是「递归 CTE」,用查询表达循环——写法完全不一样。
  用 Python 复刻这段循环,输出:1、2、3、4、5
  复刻的是「循环 5 次、每次打印」这个语义,不是 T-SQL 的语法。
  实际项目里,能用一条 SQL 解决的不要写循环——逐行处理比集合操作慢一个量级。

  ④ CONTINUE、BREAK、RETURN、GOTO
  --------------------------------------------------------------------
| 关键字 | 作用 | 该不该用 |
|---|---|---|
| CONTINUE | 跳过本次,进入下一轮 | 可以用,注意死循环 |
| BREAK | 跳出整个循环 | 可以用 |
| RETURN | 立即退出,可带返回码 | 存储过程里常用 |
| GOTO | 跳到指定标签 | 不要用 |

  GOTO 是唯一一个「教材列了但建议别用」的。
  它让控制流可以往任何方向跳,代码读起来像一团线——现代语言里基本都被废弃了。
  T-SQL 保留它,是为了兼容几十年前的老代码。新写的代码用 WHILE 和 IF 就够了。
  RETURN 在存储过程里可以带一个整数返回码,0 表示成功,非 0 是错误码——
  这一点跟 C 语言的习惯一样。

CASE 这段能真跑,是因为它是标准 SQL。 五行数据分成了四档——注意 92 是「优秀」不是「良好」, 因为 CASE 从上往下匹配,>= 90 先命中。 如果把 >= 60 那个分支挪到最前面,结果会全变成「及格」。这不是 bug, 是 CASE 的定义——条件顺序就是优先级。 IF 和 WHILE 那两段只能贴原码。 本沙箱没有 SQL Server, Python 那个 while 复刻的是「循环 5 次、每次打印」的语义,不是 T-SQL 的语法。 GOTO 那段没写代码,只列了表。 因为它在现代 SQL 里不该出现—— 看到了就当技术债,能重构就重构。

30 秒实验

  1. 把 CASE 里的条件顺序倒过来写(先 >= 60 再 >= 90),看结果变成什么。 这就是「从上往下匹配」的直接证据——顺序错了,逻辑就全错了。
  2. 写一段 WHILE 循环,往一张表里插一万行(一行一行插),再跟一条 INSERT ... SELECT 比耗时。 差距会让你以后都不想写循环。
  3. 想一想:为什么 CASE 能写在 SELECT 里,而 IF 不能? 因为 CASE 是表达式,返回一个值;IF 是语句,控制流程。表达式能放在任何「需要值」的位置, 语句不行。

常见误区

  1. 「IF 后面可以直接跟多条语句」。只能跟一条。 要跟多条必须 BEGIN...END,漏了不报错,只是后面的语句无条件执行。 这是本节最贵的一个错。
  2. 「CASE 的条件顺序无所谓」。顺序就是优先级。 把范围大的条件写在前面,后面的分支永远走不到,而且不报错、不警告——只是结果悄悄不对。
  3. 「WHILE 能做的事,一条 SQL 做不了」。绝大多数时候反过来。 逐行循环比集合操作慢一个量级,而且代码更长、更容易错。 先想「能不能用一条 SQL」,不行再写循环。
自测三问(点开对答案) 1. 简单 `CASE` 和搜索 `CASE` 有什么区别? —— **简单 `CASE` 是 `CASE 列 WHEN 值 THEN 结果`,做等值判断;** **搜索 `CASE` 是 `CASE WHEN 条件 THEN 结果`,能做范围判断。** **两种都能写在 `SELECT` 里,因为 `CASE` 是表达式。** 2. `CASE` 里条件顺序写反了会怎样? —— **结果全错,而且不报错。** 因为它从上往下匹配,第一个成立的就返回。 **把 `>= 60` 写在 `>= 90` 前面,所有及格以上都会变成「及格」。** 3. 为什么说「能用 SQL 就别用循环」? —— **性能差一个量级。** 集合操作是内核批量处理,循环是逐行处理(RBAR)。 **而且循环代码更长、更容易出错**——除非是分批处理海量数据这种特殊场景。

下一篇 6.6 讲注释和 GO。 GO 不是 SQL 语句,是客户端的分隔符——这句话解释了「为什么变量的作用域会被切断」, 也解释了「为什么程序里不能带 GO」。

相关笔记