四组运算符:算术、比较、逻辑、位。每组都有一个「和别的语言不一样」的地方。
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 秒实验
- 把
7 / 2改成CAST(7 AS REAL) / 2再跑一次,看两边是否一致。这次一致了,都是 3.5。 所以「跨库要做除法」的正确写法是显式转型。 - 跑一下
SELECT '10' + 0和SELECT 'abc' + 0,想想「字符串转数字」失败时给什么。 在 MySQL 里都按「能转多少算多少」处理:'abc'转成 0。 这个特性让WHERE 字符串列 = 数字变成一场灾难,第 5 章末尾说过。 - 算一下
权限 & ~4在「权限 = 15」时等于多少。 11。因为 15 是 1111,把第 3 位(值 4)清掉就是 1011,也就是 11。
常见误区
- 「
7 / 2应该是 3。」 在 MySQL 里是 3.5000。 要整除写DIV,要小数就让它自然除。 - 「
WHERE 列 = NULL能查出空值。」 一行都查不出来。 空只能用IS NULL判断,=碰上 NULL 得的是 NULL,不是真。 - 「
||在哪都是拼字符串。」 MySQL 里是逻辑或,SQLite 里是拼字符串,Oracle 里也是拼字符串。 要拼字符串写CONCAT。 - 「
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 讲流程控制语句—— 运算符解决「怎么算」,流程控制解决「按条件选一条路」。
- ☐ (本篇完,下一篇 7.2 流程控制语句)