# 加上 WHERE：让数据库一行一行判断，只留下你要的那几行

条件怎么写、AND 和 OR 为什么必须加括号，以及 NULL 为什么不能用等号判断

> WHERE · NULL 处理 · 条件筛选 · 约 8 分钟 · 10 月 04 日

## 本篇要点

1. `WHERE` 放在 `FROM` 后面，对每一行算一次条件：结果为真才保留，为假或未知都丢掉。
2. 条件用比较运算符连接字段和值：数字直接写，文本和日期要加单引号；双引号在多数数据库里是给表名、字段名用的，不要拿它包字符串。
3. 日期字段带时分时，`created_at = '2024-03-01'` 等于“等于当天 0 点”，会漏掉当天的记录；取整天要写成 `>= '2024-03-01' AND < '2024-03-02'` 这样的半开区间。
4. `AND` 要求全真、结果只会变少，`OR` 只要一真、结果只会变多，`NOT` 取反。
5. 数据库固定先算 `AND` 再算 `OR`，所以 `A OR B AND C` 被读成 `A OR (B AND C)`，不报错但会多留下不该留下的行；只要混用 `AND` 和 `OR` 就加括号分组。
6. `NULL` 表示“这里没有值”，它不参与比较：`city = '杭州'`、`city <> '杭州'` 遇到空值都返回未知，而 `WHERE` 不保留未知的行，所以用一个 `<>` 条件去“排除某个值”会连带丢掉空值行。
7. 判断有没有值必须用 `IS NULL`、`IS NOT NULL`，它们永远返回真或假；写成 `= NULL` 永远不成立，还会得到空结果且不报错。
8. `NULL` 与逻辑运算的组合里要记住四条：真 AND 未知得未知，假 AND 未知得假，真 OR 未知得真，假 OR 未知得未知。
9. `WHERE` 逐行判断，在排序和统计之前完成，所以 `SELECT` 里用 `AS` 起的别名不能在 `WHERE` 中使用。

---

上一篇你写出的第一条查询是 `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 的订单：

```sql
SELECT order_id, amount
FROM orders
WHERE amount > 100;
```

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

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

<figure>

```mermaid
flowchart LR
  T["orders 表的每一行"] --> Q{"amount > 100 ？"}
  Q -->|"真"| K["留在结果里"]
  Q -->|"假或未知"| D["丢掉"]
```

</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 日一整天，就写成一段区间：

```sql
WHERE created_at >= '2024-03-01'
  AND created_at <  '2024-03-02'
```

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

## 多个条件：AND、OR、NOT

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

- `AND`：两边都成立才算通过。**结果只会变少**，每加一个 `AND` 就是再加一道筛子。
- `OR`：任一边成立就算通过。**结果只会变多**。
- `NOT`：把这个条件反过来。

```sql
SELECT order_id, city, amount
FROM orders
WHERE city = '杭州'
  AND amount > 100;
```

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

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

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

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

```sql
SELECT order_id, city, amount
FROM orders
WHERE city = '杭州' OR city = '上海' AND amount > 100;
```

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

```sql
WHERE 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` 只要一边为真就通过，金额条件根本没参与它的判断。

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

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

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

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

## NULL：不是一个值，是“这里没有值”

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

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

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

```sql
SELECT order_id, city, amount
FROM orders
WHERE city <> '杭州';
```

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

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

```sql
WHERE city <> '杭州' OR city IS NULL
```

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

`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 的订单，只要订单号、城市和金额三列。

<details>
<summary>参考答案</summary>

```sql
SELECT order_id, city, amount
FROM orders
WHERE created_at >= '2024-03-01'
  AND created_at <  '2024-04-01'
  AND (city = '杭州' OR city = '上海')
  AND amount >= 100;
```

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

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

</details>

## 术语表

- `WHERE` 子句：写在 `FROM` 之后的条件，数据库对每一行算一次，只保留判断为真的行。
- 比较运算符：`=`、`<>` 或 `!=`、`>`、`<`、`>=`、`<=`，把字段和值连成一条条件。
- 逻辑运算符：`AND`（全真才通过，结果变少）、`OR`（一真就通过，结果变多）、`NOT`（取反）。
- 运算符优先级：`AND` 固定先于 `OR` 计算，所以混用两者时要用括号分组，否则语句能跑但结果是错的。
- `NULL`：表示某个字段没有填值，和空字符串、数字 0 都不同；它参与任何比较都得到“未知”。
- 三值逻辑：条件的结果有真、假、未知三种，`WHERE` 只留下真，未知与假一样会被丢掉。
- `IS NULL` / `IS NOT NULL`：专门判断“有没有值”的写法，结果只会是真或假，不会返回未知。

## 来源

1. [MySQL Reference Manual: Working with NULL Values — 不能用 =、<> 测试 NULL，必须用 IS NULL / IS NOT NULL](https://dev.mysql.com/doc/refman/8.4/en/working-with-null.html)
2. [PostgreSQL Documentation: Comparison Functions and Operators — 与 NULL 的普通比较返回“未知”，不要写 expression = NULL](https://www.postgresql.org/docs/current/functions-comparison.html)
3. [PostgreSQL Documentation: Operator Precedence — NOT、AND、OR 的先后顺序与括号分组](https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-PRECEDENCE)
4. [SQL Server 文档：运算符优先级 — 括号可以覆盖默认优先级](https://learn.microsoft.com/en-us/sql/t-sql/language-elements/operator-precedence-transact-sql)

---

原文：https://pangzhengboyin.com/articles/sql-where-filtering-and-null-a4d37517

> **庞征博引** · 想学的，慢慢都会
>
> 庞征博引是把想学的东西写成连载的 AI 学习工具。说出想学什么，它会先了解你的基础，再把主题写成一篇篇 5–10 分钟能读完的文章；边读边问，接下来学什么跟着你走。这篇就是这样写出来的。
>
> 开始你自己的连载 → https://pangzhengboyin.com
