上一篇你写出的第一条查询是 SELECT order_id, amount FROM orders;,它把整张表的每一行都取回来了。可现实里的取数问题几乎都带条件——“只看 3 月的”“只看杭州和上海”“只看金额上百的”。这一篇就讲 WHERE:把条件写出来,让数据库逐行判断,只留下通过的那几行。

还是用上一篇那张 orders 表,这一篇里它多了两行数据(NULL 表示这一格是空的):

order_iduser_idcityamountcreated_at
10018823杭州129.002024-03-01 10:23:11
10029107上海58.502024-03-01 11:07:42
10038823杭州320.002024-03-02 09:15:03
10049310NULL76.002024-03-02 14:02:50
10059107杭州45.002024-03-03 08:40:12

WHERE 做的事:逐行算一个判断

想知道金额超过 100 的订单:

1SELECT order_id, amount
2FROM orders
3WHERE amount > 100;

WHERE 写在 FROM 后面,后面接一个条件。数据库拿到这张表后,对 每一行 算一次 amount > 100:判断为真就保留这一行,判断为假就丢掉。算完的结果是 1001(129.00)和 1003(320.00)两行——1004 的 76.00、1005 的 45.00 都因为没超过 100 被丢掉。

把它和上一篇的写法放在一起看,整条查询的结构其实只有三段:SELECT 决定结果要哪几列,FROM 决定去哪张表,WHERE 决定留哪几行。

<figure>
绘制中
</figure>

条件的左边和右边怎么写

条件通常是“某个字段”和“某个值”比大小或比相等,中间用比较运算符:

  • = 等于,<> 或 != 不等于(两种写法完全等价)
  • >、<、>=、<= 大于、小于、不小于、不大于

左边的写法你在上一篇已经会了,就是字段名。右边要按字段的类型来写:

数字直接写,不加引号。 amount > 100、amount >= 100。

文本和日期要加单引号。 city = '杭州'、created_at >= '2024-03-01'。这里有个新手常犯的错:SQL 里的单引号是“这是一段文字或一个日期”的记号,而双引号在大多数数据库里是给表名、字段名用的,不是给值用的。MySQL 默认两种引号都当字符串处理,所以你在 MySQL 里用双引号写 city = "杭州" 能跑通,但换到 PostgreSQL、SQLite 就会报错甚至理解成别的东西。统一用单引号最稳。

日期字段要特别小心。 created_at 是带时分的,WHERE created_at = '2024-03-01' 的意思是“等于 3 月 1 日 0 点 0 分 0 秒”,而 1001 那条记录是 10 点 23 分下的单,所以这一条写下去会一条都取不到。要取 3 月 1 日一整天,就写成一段区间:

1WHERE created_at >= '2024-03-01'
2  AND created_at <  '2024-03-02'

左边带等号、右边不带,是因为 3 月 2 日 0 点整这一瞬间已经属于第二天。这种“含头不含尾”的写法既不会漏掉当天的最后一笔,也不会把第二天的第一笔算进来。

多个条件:AND、OR、NOT

一个条件往往不够用,三个逻辑运算符可以把你脑子里的问题原样写下来:

  • AND:两边都成立才算通过。结果只会变少,每加一个 AND 就是再加一道筛子。
  • OR:任一边成立就算通过。结果只会变多。
  • NOT:把这个条件反过来。
1SELECT order_id, city, amount
2FROM orders
3WHERE city = '杭州'
4  AND amount > 100;

上面这条取杭州的大额订单,只留下 1001 和 1003——1005 的城市虽然对,金额 45.00 不够,一样被筛掉。把 AND 换成 OR,意思就变成“杭州的,或者金额超过 100 的”,1005 就进来了。1004 呢?它的城市是空的、金额也不到 100,仍然不会出现,原因在下面讲 NULL 的那一节。

一行一个条件、AND 写在行首,是最实用的排版习惯:以后加一个条件或者临时注释掉一个,都不用重新调格式。

混用 AND 和 OR:括号不是选做题

现在问题来了。想取“杭州或上海、并且金额超过 100 的订单”,很自然会写成:

1SELECT order_id, city, amount
2FROM orders
3WHERE city = '杭州' OR city = '上海' AND amount > 100;

这条语句不会报错,但结果是错的。数据库对 AND 和 OR 的先后顺序有固定规定:先算 AND,再算 OR,跟算术里“先乘除后加减”是一个道理。所以上面这句会被读成:

1WHERE city = '杭州' OR (city = '上海' AND amount > 100)

逐行算一遍就清楚了(用上面 5 行数据):

行city = '杭州'city = '上海' AND amount > 100整句结果
1001 杭州 129真假真,保留
1002 上海 58.5假假假,丢弃
1003 杭州 320真假真,保留
1004 空 76未知假未知,丢弃
1005 杭州 45真假真,保留

1005 金额只有 45 元,本来不该出现,但它落在 city = '杭州' 这个分支里,而 OR 只要一边为真就通过,金额条件根本没参与它的判断。

要得到想要的结果,用括号把“城市这一组”圈起来:

1WHERE (city = '杭州' OR city = '上海')
2  AND amount > 100

这样先算出“是不是杭州或上海”,再要求金额超过 100,1005 就被筛掉了。

规则可以简化成一句:只要 AND 和 OR 出现在同一个 WHERE 里,就用括号把每一组条件圈清楚。 即使不加括号结果也对,也建议加——它省掉的是几个月后你或同事重新推演优先级的时间。

NULL:不是一个值,是“这里没有值”

看 1004 那一行,city 那一格是空的。数据库用 NULL 表示“这个字段没有值”。它跟“空字符串”和“0”都不是一回事:空字符串是一个长度为零的文字,0 是一个数字,而 NULL 是 根本没有填。

关键的一点是:NULL 不参与比较。对 1004 这一行,city = '杭州' 返回的既不是真也不是假,而是一个第三态——未知(UNKNOWN)。WHERE 只留下判断为真的行,所以未知和假一样,都会被丢掉。

丢掉 1004 在这里正合我意,它确实不是杭州。麻烦出在反过来的问法上。想“排除杭州的订单”,很容易写成:

1SELECT order_id, city, amount
2FROM orders
3WHERE city <> '杭州';

你以为会拿到 1002、1004、1005 三条,实际只回来 1002 一条。因为 1005 的 city <> '杭州' 是假,而 1004 的 city <> '杭州' 是未知——未知同样不留。NOT (city = '杭州') 也救不了,因为 NOT 未知还是未知。

正确写法是把“没有值”这件事单独说出来:

1WHERE city <> '杭州' OR city IS NULL

IS NULL 和 IS NOT NULL 是专门用来判断“有没有值”的两个写法,它们 永远返回真或假,不会返回未知。千万不要写 city = NULL:任何跟 NULL 比较的运算符(=、<>、<、>)结果都是未知,条件永远不成立,你会得到一张空结果表,还不报错——MySQL、PostgreSQL 的官方文档都专门提醒过这一点12。

NULL 混进 AND、OR 之后的规则,只有下面四种搭档需要记住:

组合结果
真 AND 未知未知
假 AND 未知假
真 OR 未知真
假 OR 未知未知

有了这张表,前面两处“未知”就能自己算出来:1004 在两个待选条件的上下文中都落进未知,整句就不是真,于是不留。这也是实际取数里很常见的一种状况——一个带空值的行,可能因为另一个分支为真被保留下来,也可能因为整个条件落进未知而被丢掉。所以当你发现“行数比预想的少”,而某个字段恰好有大量空值时,第一件要查的就是它有没有被 <> 或 NOT 排除掉。

最后记一条顺序上的事:WHERE 是逐行判断,在排序、统计之前就完成了。这也解释了一个容易撞上的报错——上一篇你用 AS 给结果列起的别名,在 WHERE 里不能直接用,因为那一列的名字这时候还不存在,得写原来的字段名。

小结和练习

WHERE 的作用是逐行判断、只留真行。写条件时,左字段右值,文本和日期用单引号,日期区间用“含头不含尾”。多个条件用 AND、OR、NOT 组合,只要混用了 AND 和 OR 就加括号。遇到可能为空的字段,别用 =、<> 去跟 NULL 较劲,用 IS NULL、IS NOT NULL。

练习题:取出 2024 年 3 月(3 月 1 日 0 点到 4 月 1 日 0 点之间)下单、来自杭州或上海、且金额不低于 100 的订单,只要订单号、城市和金额三列。

参考答案
1SELECT order_id, city, amount
2FROM orders
3WHERE created_at >= '2024-03-01'
4  AND created_at <  '2024-04-01'
5  AND (city = '杭州' OR city = '上海')
6  AND amount >= 100;

结果只有 1001 和 1003 两行。可以自己对照 5 行数据验一遍:1002 是上海但金额 58.50 不够;1004 城市为空,city = '杭州' OR city = '上海' 得到未知,整组 AND 之后仍然不是真;1005 是杭州但金额 45.00 不够。

注意城市那一组的括号不能省,也不能写成 city = '杭州' OR city = '上海' AND amount >= 100。