上一篇你写出的第一条查询是 SELECT order_id, amount FROM orders;,它把整张表的每一行都取回来了。可现实里的取数问题几乎都带条件——“只看 3 月的”“只看杭州和上海”“只看金额上百的”。这一篇就讲 WHERE:把条件写出来,让数据库逐行判断,只留下通过的那几行。
还是用上一篇那张 orders 表,这一篇里它多了两行数据(NULL 表示这一格是空的):
| order_id | user_id | city | amount | created_at |
|---|---|---|---|---|
| 1001 | 8823 | 杭州 | 129.00 | 2024-03-01 10:23:11 |
| 1002 | 9107 | 上海 | 58.50 | 2024-03-01 11:07:42 |
| 1003 | 8823 | 杭州 | 320.00 | 2024-03-02 09:15:03 |
| 1004 | 9310 | NULL | 76.00 | 2024-03-02 14:02:50 |
| 1005 | 9107 | 杭州 | 45.00 | 2024-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 决定留哪几行。
条件的左边和右边怎么写
条件通常是“某个字段”和“某个值”比大小或比相等,中间用比较运算符:
=等于,<>或!=不等于(两种写法完全等价)>、<、>=、<=大于、小于、不小于、不大于
左边的写法你在上一篇已经会了,就是字段名。右边要按字段的类型来写:
数字直接写,不加引号。 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 NULLIS 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。