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

四组运算符:算术、比较、逻辑、位。每组都有一个「和别的语言不一样」的地方。 NULL 不是值,是一个状态;碰到它的比较全变成「不知道」。 7 / 2 在 MySQL 里是 3.5000,|| 在 MySQL 里是逻辑或——两处都能跨库出事。

📝 本章和前面几章的关系

第 6 章把表和库都建好了,从这一章开始往里写数据、算数据。 运算符是所有 SQL 的零件:WHERE、HAVING、SET、CHECK 里都是它。 这一篇的三值逻辑,会在第 9 章讲 IS NULL 时再撞一次,第 11 章讲索引失效时还会撞。

一、四组运算符,四个坑

教材 7.1 把运算符分成四组,这一篇按「坑在哪」重排。

组 有哪些 那个坑
算术 + - * / DIV % / 是真除法,不是整除
比较 = <=> <> > < BETWEEN IN LIKE IS NULL 碰上 NULL 全变「不知道」
逻辑 AND OR NOT XOR 要算三种状态,不是两种
位 & | ^ ~ << >> 按整数位算,结果常常看不懂

第二行那个坑是四组里最贵的。 它不会报错,只会悄悄少查几行——而少查的那几行,往往就是要找的那几行。 第一行和第二行的坑加起来,构成了 SQL 里多数的「明明有数据却查不出来」。 所以这一篇的重点不是记运算符,是记这两个坑。

二、算术:除法、取模、溢出

/ 在 MySQL 里永远按小数除。7 / 2 给 3.5000,不是 3。 要整除必须写 DIV:7 DIV 2 给 3。 这一条和 C、Java、Python 2 的整数除法都不一样,是很多人第一次写 SQL 就踩的坑。 尤其是算「平均值」时:SUM(a) / COUNT(*) 在 MySQL 里天然带小数,不用转型。 % 和 MOD 是同一个运算的两种写法,都只对整数有意义。 被除数或除数有一个是 NULL,结果就是 NULL,不是 0。 溢出这一项要单独提:MySQL 的整数运算超范围会报错,不会静默变成负数。 SELECT 9223372036854775807 + 1 会报 BIGINT value is out of range。 这一点和 C 不一样,是好事——它逼你在出问题的地方改。

三、比较:NULL 让一切都变成「不知道」

这一段是这一篇最重要的一段。 NULL 不是「空字符串」,也不是 0,它是「这个值不知道」。

表达式 结果 为什么
NULL = NULL NULL 两个都不知道,没法判断相等
NULL <> NULL NULL 同上,不相等也判断不了
NULL IS NULL 真 IS 专门用来问「是不是空」
1 + NULL NULL 任何运算碰了 NULL 都是 NULL
NULL <=> NULL 真 MySQL 专有的「空值安全等于」

第一、二行合起来是一条硬规矩:判断空只能用 IS NULL 或 IS NOT NULL,不能用 =。 WHERE 列 = NULL 永远一行都查不出来。 因为每一行算出来都是 NULL,而 NULL 不算「满足」。 这一条造成的 bug 极其常见,而且特别难发现:语句不报错,就是没数据。 所以写完 WHERE 之后,凡是涉及可能为空的列,都要检查一遍用的是 = 还是 IS。 第五行那个 <=> 是 MySQL 独有的。 它把 NULL 当成一个普通值来比,NULL <=> NULL 给真,1 <=> NULL 给假。 用处是「比较两列是否完全一样,包括都为空」的场景。

四、逻辑:三值逻辑,不是两值

AND 和 OR 的真值表不是 4 格,是 9 格,因为有第三种状态。

真 假 空
真 真 假 空
假 假 假 假
空 空 假 空

这张表里最有用的是第二行和第三行。 「假 AND 空」是假——因为假 AND 任何东西都必然是假,不用管另一边。 「真 AND 空」是空——真的那边已经确定了,另一边不知道,整体就不知道。 OR 的表正好反过来:只要有一边是真,整体就是真,不管另一边是不是空。 这两张表合起来说明一件事:AND 和 OR 在能确定结果时,就不看另一边了。 这叫「短路」,和编程语言里的 &&、|| 一样。 实践含义是:WHERE a = 1 AND b = 2 里,如果 b 是 NULL,这一行就不满足。 所以「查 a 等于 1 且 b 不等于 2」这种条件,会把 b 为 NULL 的行漏掉——它们既不等于 2, 也不「不等于 2」。 要带上它们就得写 (b <> 2 OR b IS NULL)。

五、位运算:权限掩码其实是老朋友

位运算在业务里最典型的用法就是权限。 第 3 章那套 SEL/INS/UPD/DEL 取 1、2、4、8,就是四个二进制位。

运算 写法 用在权限上的意思
与 权限 & 4 有没有「改」这个权限
或 权限 | 4 把「改」加上
异或 权限 ^ 4 切换「改」这个权限
取反再与 权限 & ~4 把「改」去掉

四行合起来是一套完整的「开关」操作。 1 << n 就是「第 n 位」,所以四个权限可以写成 1 << 0 到 1 << 3。 但要说清楚代价:位运算省空间,可读性差,而且用不上索引。 「查有写权限的所有用户」这种需求,位掩码要走全表扫。 所以真实的权限系统还是用关联表,位掩码只用在「一次取出、内存里判断」的场景。

六、优先级:加括号比背表省事

教材 7.1 有一张优先级表,十几行,不好背也不用背。 记住两句就够:算术高于比较,比较高于逻辑。

表达式 实际算的是 结果
1 + 2 * 3 1 + (2 * 3) 7
NOT 1 = 1 NOT (1 = 1) 0
2 + 3 & 1 (2 + 3) & 1 1
1 = 1 AND 2 = 2 (1=1) AND (2=2) 1
NOT 1 AND 0 (NOT 1) AND 0 0

第二行是最常错的一行:NOT 1 = 1 不是「非 1 等于 1」,是「1 等于 1 这件事为假」。 因为 = 的优先级高于 NOT,所以先算等号。 第三行也一样:算术运算符整体比位运算符高,所以先算加法。 这种题看一遍就够,不必记——写的时候加括号就行。 括号不花钱,猜优先级要花时间,还可能猜错。 凡是 WHERE 里有两个以上 AND/OR 混用,一律加括号,这是团队规范级别的建议。

七、动手 Demo:运算符的每一个坑都真跑一遍

这一课把四组运算符各跑一遍,每一行都对照 MySQL 的答案。 两边不一样的会明确标出来——这些地方换库就会出 bug。 沙箱是 SQLite,所以有几处它不支持(DIV、MOD、^),也照实打出来。

def ev(c, expr):
    try:
        return c.execute("SELECT " + expr).fetchone()[0]
    except Exception as e:
        return "不支持(%s)" % str(e).split(":")[0]

# 一、算术:7/2 两边不一样
print(ev(c, "7 / 2"), ev(c, "7 DIV 2"), ev(c, "7 % 2"))

# 二、NULL 传染
print(ev(c, "NULL = NULL"), ev(c, "NULL IS NULL"))

# 三、三值逻辑:九个组合全打出来
for a in ("1", "0", "NULL"):
    for b in ("1", "0", "NULL"):
        print(a, "AND", b, ev(c, "%s AND %s" % (a, b)))

# 四、|| 在两边是两件事
print(ev(c, "'a' || 'b'"))          # ab,MySQL 给 0

# 五、优先级
print(ev(c, "NOT 1 = 1"), ev(c, "2 + 3 & 1"))

实跑输出:

--------------------------------------------------------------------
第三十一课:运算符的每一个坑都真跑一遍
--------------------------------------------------------------------

  **教材 7.1 把运算符分成四组:算术、比较、逻辑、位。**
  **四组里每一组都有一个「和别的语言不一样」的地方。**
  **这一课把每个运算符都真算一遍,算完逐个对照 MySQL 的答案。**
  **凡是两边不一样的,都会明确标出来——这些地方换库就会出 bug。**

  一、算术:除法先分叉
  --------------------------------------------------------------------

  表达式   本次实测            MySQL 的答案
  -----------------------------------------
  7 + 3    10                  10
  7 - 3    4                   4
  7 * 3    21                  21
  7 / 2    3                   3.5000
  7 DIV 2  不支持(near "2")  3
  7 % 2    1                   1
  7 MOD 2  不支持(near "2")  1
  **第四行就是那道老题:`7 / 2` 在 MySQL 里是 3.5000,不是 3。**
  **MySQL 的 `/` 永远按小数除,要整除得写 `DIV`。**
  **这里给 3,是因为 SQLite 两个整数相除就按整数算。**
  **所以「同一个表达式,换个库结果从 3 变 3.5」是真事,做除法时要么显式转型,要么统一用 `DIV`。**
  **`MOD` 是 MySQL 的写法,这个沙箱只认 `%`,两个是同一个运算。**
  **还有一条:被除数或除数有一个是 NULL,结果就是 NULL,不是 0。**

  二、NULL 传染:碰一下就变 NULL
  --------------------------------------------------------------------

  表达式            结果
  ----------------------
  NULL + 1          NULL
  NULL = NULL       NULL
  NULL <> NULL      NULL
  NULL IS NULL      1
  NULL IS NOT NULL  0
  1 + NULL          NULL
  **六行里五个是 NULL,只有 `IS NULL` 那两行给了真值。**
  **`NULL = NULL` 不是真也不是假,是「不知道」——所以它给 NULL。**
  **第三行最反直觉:两个空值不相等,也不「不相等」。**
  **要判断空只能用 `IS NULL`,不能用 `= NULL`。**
  **`WHERE 列 = NULL` 永远一行都查不出来,这是 SQL 里最常见的一类 bug。**
  **MySQL 还多一个 `<=>`:空值安全等于。**
  **`NULL <=> NULL` 给它 1,`1 <=> NULL` 给它 0。**
  **SQLite 没有这个运算符,等价写法是 `IS`。**

  [教材原码 · 本沙箱不执行]
  | SELECT NULL <=> NULL, 1 <=> NULL;
  ① 教材原码,本沙箱不执行


  三、三值逻辑:AND 和 OR 的真值表
  --------------------------------------------------------------------
  **AND 的九种组合,这里一行一行打出来:**

  表达式     结果
  ---------------
  真 AND 真  1
  真 AND 假  0
  真 AND 空  NULL
  假 AND 真  0
  假 AND 假  0
  假 AND 空  0
  空 AND 真  NULL
  空 AND 假  0
  空 AND 空  NULL
  **九行里最关键的是带「空」的那五行。**
  **「真 AND 空」是空,「假 AND 空」是假——因为假 AND 任何东西都必然是假,不用管另一边。**
  **这就是「三值逻辑」:除了真和假,还有「不知道」。**
  **实践含义是:`WHERE` 里只要有一行碰上 NULL,那一行就不满足条件。**
  **所以「字段 <> 'X'」会漏掉字段为 NULL 的那些行——它们既不是 X,也不「不等于 X」。**

  四、位运算:权限掩码其实是老朋友
  --------------------------------------------------------------------

  表达式   本次实测                      MySQL 的答案
  ---------------------------------------------------
  6 & 3    2                             2
  6 | 3    7                             7
  6 ^ 3    不支持(unrecognized token)  5
  ~6       -7                            -7
  1 << 4   16                            16
  16 >> 2  4                             4
  **第五行是位运算最实用的地方:`1 << n` 就是「第 n 位」。**
  **第 3 章那套权限位掩码 SEL/INS/UPD/DEL 取 1、2、4、8,就是这么来的。**
  **第三行的 `^` 在这里跑不了,因为 SQLite 没有位异或这个运算符;MySQL 有,给 5。**
  **要位异或得绕成 `(a | b) - (a & b)`,这也是跨库搬 SQL 时会踩的坑。**
  **第四行的 `~6` 给 -7,是因为按位取反在补码里等价于「取负再减一」。**
  **这一条不用记,知道位运算是在整数上做就行。**
  **位运算的一个真用途:一个字段存多种状态。**
  **`flags & 4` 判断「有没有第四种」,`flags | 4` 加上它,`flags & ~4` 去掉它。**
  **比起加四个布尔列,它省空间,但可读性差,索引也用不上。**

  五、|| 在两个引擎里是两件事
  --------------------------------------------------------------------

  表达式      本次实测  MySQL 的答案
  ----------------------------------
  'a' || 'b'  ab        0
  1 || 0      10        1
  0 || 0      00        0
  **第一行是这一课最该记住的一行。**
  **`'a' || 'b'` 在这里拼成 `ab`,在 MySQL 里给 0。**
  **原因:`||` 在 SQLite 里是字符串连接,在 MySQL 里是逻辑或的简写。**
  **MySQL 把 `'a'` 和 `'b'` 都转成数字 0,0 或 0 还是 0。**
  **所以带 `||` 的 SQL 绝对不能跨库搬。**
  **要拼字符串就写 `CONCAT`,它在两边含义相同;要逻辑或就写 `OR`。**

  六、优先级:不加括号的地方最容易错
  --------------------------------------------------------------------

  表达式           结果  为什么
  -----------------------------------
  1 + 2 * 3        7     乘除先于加减
  (1 + 2) * 3      9     括号最优先
  NOT 1 = 1        0     = 先于 NOT
  2 + 3 & 1        1     + 先于 &
  1 = 1 AND 2 = 2  1     = 先于 AND
  NOT 1 AND 0      0     NOT 先于 AND
  **第三行是最常错的一行:`NOT 1 = 1` 不是 `(NOT 1) = 1`,是 `NOT (1 = 1)`。**
  **因为 `=` 的优先级高于 `NOT`,所以先算等号,结果是 0。**
  **第四行也一样:`2 + 3 & 1` 是 `(2+3) & 1`,不是 `2 + (3&1)`。**
  **算术运算符整体比位运算符高。**
  **这张表的用处不是背,是提醒你:写 SQL 时凡是有两个以上运算符,就加括号。**
  **括号不花钱,猜优先级要花时间,还可能猜错。**

第一段那张表里,7 / 2 是这一课的核心:这里给 3,MySQL 给 3.5000。 DIV 和 MOD 在这里跑不了,报的是语法错,因为 SQLite 只认 / 和 %。 但它们的 MySQL 答案(3 和 1)是对的,对照着看就行。 第二段六行里五个是 NULL,只有 IS NULL 那两行给了真值。 NULL = NULL 给 NULL 而不是 0,这是「不知道」的样子——不是假,是没法判断。 第三段是九宫格,一行一行打出来的。 看那五行带「空」的:假 AND 空 给 0,真 AND 空 给 NULL。 前者能确定(假 AND 什么都假),后者确定不了。 第四段两行最有意思:'a' || 'b' 在这里是 ab,在 MySQL 里是 0。 同一个符号,一个是拼字符串,一个是逻辑或。 带 || 的 SQL 绝对不能跨库搬,这条要记住。 第五段:NOT 1 = 1 给 0,2 + 3 & 1 给 1。 两个都证明了「先算优先级高的那个」,而不是从左往右。

30 秒实验

  1. 把 7 / 2 改成 CAST(7 AS REAL) / 2 再跑一次,看两边是否一致。这次一致了,都是 3.5。 所以「跨库要做除法」的正确写法是显式转型。
  2. 跑一下 SELECT '10' + 0 和 SELECT 'abc' + 0,想想「字符串转数字」失败时给什么。 在 MySQL 里都按「能转多少算多少」处理:'abc' 转成 0。 这个特性让 WHERE 字符串列 = 数字 变成一场灾难,第 5 章末尾说过。
  3. 算一下 权限 & ~4 在「权限 = 15」时等于多少。 11。因为 15 是 1111,把第 3 位(值 4)清掉就是 1011,也就是 11。

常见误区

  1. 「7 / 2 应该是 3。」 在 MySQL 里是 3.5000。 要整除写 DIV,要小数就让它自然除。
  2. 「WHERE 列 = NULL 能查出空值。」 一行都查不出来。 空只能用 IS NULL 判断,= 碰上 NULL 得的是 NULL,不是真。
  3. 「|| 在哪都是拼字符串。」 MySQL 里是逻辑或,SQLite 里是拼字符串,Oracle 里也是拼字符串。 要拼字符串写 CONCAT。
  4. 「NOT 的优先级最高,所以 NOT a = b 是先算 NOT a。」 = 比 NOT 高,所以是 NOT (a = b)。 要写「非 a 等于 b」必须自己加括号。
自测三问(点开对答案) 1. `NULL = NULL` 返回什么? —— **NULL,不是真也不是假。** **两个「不知道」没法比较。要问是不是空只能用 `IS NULL`。** 2. `假 AND NULL` 和 `真 AND NULL` 分别返回什么? —— **假 AND NULL 给假,真 AND NULL 给 NULL。** **因为假 AND 什么都假,能确定;真 AND NULL 确定不了。** 3. `2 + 3 & 1` 等于几? —— **1。算术运算符比位运算符优先级高,所以先算 2 + 3 得 5,5 & 1 得 1。**

7.1 到这儿结束。下一篇 7.2 讲流程控制语句—— 运算符解决「怎么算」,流程控制解决「按条件选一条路」。

相关笔记