上一篇:《先把例子跑起来:MySQL 环境与本文的示例数据》 | 本篇:把筛选条件写对 | 下一篇:《排序、分页和去重:结果为什么每次不一样》
概念
WHERE 是 SQL 里第一个"会骗人"的地方:它看起来只是"条件为真就留下",实际上它跟三值逻辑绑在一起------ 条件的结果可以是真、假、未知 三种。未知的那一行不会被留下,也不会被"拒绝",它就是安安静静地消失了。
我为什么单独开一篇讲它?因为新手 90% 的"查不到数据"都出在这里:用的不是 IS NULL,或者被 NOT IN 里的一个 NULL 把整条条件废掉了。这两个坑我在下面都用示例表当场复现给你看。
从这一篇开始,每篇文章的示例数据都是自带的:文里会先建几张几十行以内的小表, 后面所有例子都基于它们,复制粘贴即可运行,不依赖任何外部库。
主要用法
| 写法 | 说明 | 典型场景 |
|---|---|---|
= <> > >= < <= |
普通比较 | 金额、日期、编号 |
BETWEEN a AND b |
闭区间,含两端 | 金额区间、时间段 |
IN (...) / NOT IN (...) |
集合判断;NOT IN 遇 NULL 会失效 |
枚举、白名单/黑名单 |
LIKE '张%' / LIKE '__' |
模糊匹配:% 任意多字符,_ 恰好一个字符 |
前缀搜索、定长校验 |
IS NULL / IS NOT NULL |
判空,唯一正确的写法 | 未付款、无提成、缺档案 |
AND / OR / NOT |
组合条件,AND 优先级高于 OR |
多条件筛选 |
举例
完整案例(基于本文的示例表)
第一步:把示例数据建出来。 三张小表,分别代表"员工""订单""商品":
sql
DROP TABLE IF EXISTS emp;
CREATE TABLE emp (
empno INT PRIMARY KEY,
ename VARCHAR(20) NOT NULL,
job VARCHAR(20) NOT NULL,
mgr INT NULL,
hiredate DATE NOT NULL,
sal DECIMAL(10,2) NOT NULL,
comm DECIMAL(10,2) NULL,
deptno INT NULL,
status ENUM('在职','试用','离职') NOT NULL DEFAULT '在职'
);
INSERT INTO emp (empno, ename, job, mgr, hiredate, sal, comm, deptno, status) VALUES
(1001,'张伟','总经理', NULL,'2016-03-01',32000,NULL, 10,'在职'),
(1002,'严伟','技术总监', 1001,'2017-06-15',26000,NULL, 10,'在职'),
(1003,'周超','销售总监', 1001,'2018-09-01',22000,3000, 20,'在职'),
(1004,'郑浩','高级工程师',1002,'2019-04-20',19000,NULL, 10,'在职'),
(1005,'韩静','测试工程师',1002,'2020-07-01',12000,NULL, 30,'在职'),
(1006,'李超','工程师', 1002,'2020-11-10',11000,NULL, 30,'在职'),
(1007,'张杰','销售经理', 1003,'2021-02-18',15000,2500, 20,'在职'),
(1008,'张倩','销售代表', 1007,'2021-08-05', 9800,1200, 20,'试用'),
(1009,'王强','工程师', 1002,'2022-03-12',10500,NULL, 30,'在职'),
(1010,'赵敏','会计', 1001,'2022-09-01', 9000,NULL, 40,'在职'),
(1011,'陈晨','人事专员', 1001,'2023-01-09', 8000,NULL, 50,'离职'),
(1012,'李小明','客服专员',1001,'2023-05-22', 7300,NULL, 50,'在职'),
(1013,'孙丽','前台', 1001,'2023-07-01', 8000,NULL, NULL,'在职');
DROP TABLE IF EXISTS orders;
CREATE TABLE orders (
order_id INT PRIMARY KEY,
order_no VARCHAR(32) NOT NULL,
customer_id INT NOT NULL,
order_time DATETIME NOT NULL,
status ENUM('待付款','已付款','已发货','已完成','已取消','已退款') NOT NULL,
pay_amount DECIMAL(12,2) NOT NULL,
pay_time DATETIME NULL
);
INSERT INTO orders (order_id, order_no, customer_id, order_time, status, pay_amount, pay_time) VALUES
(1,'SO2026001',1,'2026-01-05 10:12:00','已完成',1280.00,'2026-01-05 10:20:00'),
(2,'SO2026002',2,'2026-01-08 14:30:00','已完成', 560.00,'2026-01-08 14:35:00'),
(3,'SO2026003',3,'2026-02-11 09:05:00','待付款', 990.00,NULL),
(4,'SO2026004',1,'2026-02-19 20:41:00','待付款',2380.00,NULL),
(5,'SO2026005',4,'2026-03-02 11:00:00','已取消', 420.00,NULL),
(6,'SO2026006',5,'2026-03-15 16:22:00','已发货',1760.00,'2026-03-15 16:30:00'),
(7,'SO2026007',2,'2026-04-01 08:10:00','已退款',3050.00,NULL),
(8,'SO2026008',6,'2026-04-09 19:47:00','已付款', 880.00,'2026-04-09 19:50:00'),
(9,'SO2026009',7,'2026-01-31 15:00:00','已完成',1500.00,'2026-01-31 15:05:00');
DROP TABLE IF EXISTS product;
CREATE TABLE product (
product_id INT PRIMARY KEY,
name VARCHAR(40) NOT NULL,
category VARCHAR(20) NOT NULL,
price DECIMAL(10,2) NOT NULL
);
INSERT INTO product (product_id, name, category, price) VALUES
(1,'轻语 轻奢款 Plus', '手机通讯',4762.80),
(2,'原点 轻奢款 Lite', '电子产品', 99.16),
(3,'星河 经典款 Standard','电子产品',6415.10),
(4,'云栖 轻奢款 Pro', '家居生活',2318.52),
(5,'极简 经典款 Lite', '家居生活',3081.93),
(6,'北岸 旗舰款 Air', '手机通讯',5733.09);
建好之后是三张小表:
| 表 | 行数 | 里面有什么 |
|---|---|---|
emp |
13 | 员工:有提成的 3 人、没提成的 10 人,1 个人的部门号是空的 |
orders |
9 | 订单:4 单没有付款时间,1 单的 order_time 卡在 1 月 31 日 15:00 |
product |
6 | 商品:分类里有"手机通讯",商品名里没有 |
这三行不是凑数,后面每个坑都对应其中一条。
例 1:找出高薪员工。 13 个人里,工资高于 15000 的有 4 个:
sql
SELECT ename, job, sal FROM emp WHERE sal > 15000 ORDER BY sal DESC;
| ename | job | sal |
|---|---|---|
| 张伟 | 总经理 | 32000.00 |
| 严伟 | 技术总监 | 26000.00 |
| 周超 | 销售总监 | 22000.00 |
| 郑浩 | 高级工程师 | 19000.00 |
例 2:IN 与区间筛选。 10 号和 30 号部门一共 6 人;2020 年入职的 2 人:
sql
SELECT COUNT(*) AS 十号和三十号部门人数 FROM emp WHERE deptno IN (10, 30);
SELECT ename, hiredate FROM emp WHERE hiredate BETWEEN '2020-01-01' AND '2020-12-31' ORDER BY hiredate;
| 十号和三十号部门人数 |
|---|
| 6 |
| ename | hiredate |
|---|---|
| 韩静 | 2020-07-01 |
| 李超 | 2020-11-10 |
例 3:LIKE 的两个用法,外加一个我踩过的乌龙。 我以为用 %手机% 能搜到商品, 结果返回 0 行------因为"手机通讯"是分类,商品名里是"轻语 轻奢款 Plus"这种:
sql
SELECT COUNT(*) AS 商品名里有手机的 FROM product WHERE name LIKE '%手机%';
SELECT COUNT(*) AS 分类里有手机的 FROM product WHERE category LIKE '%手机%';
SELECT COUNT(*) AS 姓名恰好两个字的 FROM emp WHERE ename LIKE '__';
| 商品名里有手机的 | 分类里有手机的 | 姓名恰好两个字的 |
|---|---|---|
| 0 | 2 | 12 |
最后那个 12 说明 _ 数的是字符 :13 个人里只有"李小明"是三个字,其余 12 个都是两字姓名。 要是按字节算,中文名至少得写三个 _,那就一个也匹配不上了。
搜错字段这件事特别常见------先想清楚关键字到底在哪个列里 ,再去写 LIKE。
例 4:判空。 这两句才是我最想让你记住的:
sql
SELECT COUNT(*) AS 没有提成的员工 FROM emp WHERE comm IS NULL;
SELECT COUNT(*) AS 还没付款的订单 FROM orders WHERE pay_time IS NULL;
SELECT status, COUNT(*) AS 单数 FROM orders WHERE pay_time IS NULL
GROUP BY status ORDER BY 单数 DESC;
| 没有提成的员工 |
|---|
| 10 |
| 还没付款的订单 |
|---|
| 4 |
| status | 单数 |
|---|---|
| 待付款 | 2 |
| 已取消 | 1 |
| 已退款 | 1 |
4 这个数字,比你以为的"待付款有 2 单"多了一倍------已取消和已退款的订单也不会有付款时间。 判空查出来的东西,往往不止你以为的那一类,这是数据里很真实的细节。
30 秒小例子(复制即跑)
不建表也能验证三值逻辑,下面这段自带数据:
sql
WITH t(id, a) AS (
SELECT 1, 10 UNION ALL SELECT 2, NULL UNION ALL SELECT 3, 0
)
SELECT id,
a,
a = 0 AS 等于零,
a <> 0 AS 不等于零,
a IS NULL AS 是空值
FROM t
ORDER BY id;
第三行(a = 0)如果是 0,说明它两边都不成立------NULL 参与比较的结果是"未知",两个条件都不会命中它。 顺一句写法:WITH 名字 AS (...) 叫 CTE,就是把一段查询起个名字,第 11 篇会专门讲。
最佳实践
- 判空只用
IS NULL/IS NOT NULL。= NULL和<> NULL永远不成立,写了等于白写。 AND和OR混用时一定加括号 。a = 1 OR a = 2 AND b = 3不是你想要的意思,AND会先算。- 写
NOT IN之前先想一秒"列表或子查询里会不会有 NULL" 。有就改NOT EXISTS,或者显式排掉 NULL。 - 模糊查询别用
%关键字%去扫大表。前导通配符用不上索引,数据一大就慢得肉眼可见。 - 拿不准就先
SELECT *看一眼再改成统计。先看清命中了哪些行,比事后猜"为什么少了三条"省事得多。 - 先确认关键字在哪一列 。
%手机%搜不到商品,十有八九是它压根不在你要搜的那一列里。
易错易乱的点
① comm = NULL 什么都查不到
sql
-- 错误:结果永远 0 行,还不报错,最坑人
SELECT COUNT(*) AS 用等号判空 FROM emp WHERE comm = NULL;
-- 正确
SELECT COUNT(*) AS 正确写法 FROM emp WHERE comm IS NULL;
理由:NULL 表示"未知","未知数等于未知数"是未知,不是真。
② NOT IN 的列表里混进一个 NULL,整条条件直接废掉
sql
SELECT COUNT(*) AS 列表里带NULL FROM emp WHERE deptno NOT IN (10, 30, NULL);
SELECT COUNT(*) AS 不带NULL的 FROM emp WHERE deptno NOT IN (10, 30);
| 列表里带NULL | 不带NULL的 |
|---|---|
| 0 | 6 |
第二句是 6(13 个人里去掉 10 号、30 号部门各 3 人,再去掉部门号为空的孙丽)。 第一句是 0------多加一个 NULL,条件就再也成立不了。子查询里混进 NULL 是同一种病,下一篇细讲。
③ LIKE '_' 不是"一个字节",也不是"一个下划线"
_ 匹配任意一个字符 。想搜真正含有下划线的值,得转义:LIKE 'AB\_CD%' ESCAPE '\\'。
④ BETWEEN 是闭区间,对时间要小心右边界
sql
SELECT COUNT(*) AS BETWEEN写法 FROM orders
WHERE order_time BETWEEN '2026-01-01' AND '2026-01-31';
SELECT COUNT(*) AS 范围写法 FROM orders
WHERE order_time >= '2026-01-01' AND order_time < '2026-02-01';
| BETWEEN写法 | 范围写法 |
|---|---|
| 2 | 3 |
差的那一单,order_time 是 2026-01-31 15:00:00。BETWEEN ... AND '2026-01-31' 的上界 等于 2026-01-31 00:00:00,当天下午的单子就被漏掉了。时间条件我更推荐左闭右开的范围写法。
⑤ 中文的 LIKE 看起来"没匹配上",其实是列里带了空格
sql
SELECT '北京' = '北京 ' AS 带空格的比较, CHAR_LENGTH('北京 ') AS 实际有几个字符;
| 带空格的比较 | 实际有几个字符 |
|---|---|
| 0 | 3 |
从 Excel 导进来的数据经常带首尾空格,= '北京' 匹配不到 '北京 '。先用 TRIM() 清一遍再比。
练习题
第 2 题 查询 30 号部门之外的员工姓名和部门号,按部门号排序。
第 3 题 查询月薪在 10000 到 20000 之间、且状态为"在职"的员工。
第 4 题 查询姓名以"张"开头,或者职位包含"经理"的员工。
第 5 题 查询没有提成(comm 为空)的员工姓名与职位。
四道题都基于上面的 emp。写完对照一下:第 2 题想想"部门号是空的那个人去哪儿了", 第 4 题注意 OR 两边要各自写完整,第 5 题别写成 = NULL。
参考答案
第 2 题
sql
SELECT ename, deptno FROM emp
WHERE deptno <> 30
ORDER BY deptno, ename;
思路:9 行。<> 不会命中 NULL 行------孙丽的部门号是空的,她既不在"30 号部门"里, 也不在"30 号部门之外"里,她哪边都不算。要连她一起捞,得写:
sql
SELECT COUNT(*) AS 连部门号为空的也算上 FROM emp
WHERE deptno IS NULL OR deptno <> 30;
| 连部门号为空的也算上 |
|---|
| 10 |
第 3 题
sql
SELECT ename, sal, status FROM emp
WHERE sal BETWEEN 10000 AND 20000
AND status = '在职'
ORDER BY sal;
思路:5 行(王强、李超、韩静、张杰、郑浩)。范围 + 等值两个条件直接用 AND 串起来即可。 status 是 ENUM,值要带引号,写 status = 在职 会被当成列名。
第 4 题
sql
SELECT ename, job FROM emp
WHERE ename LIKE '张%'
OR job LIKE '%经理%'
ORDER BY ename;
思路:3 行------张伟(同时命中两个条件)、张倩、张杰。OR 的两边都是完整条件,缺一边就成了语法错误。 跑之前先想一下会不会把"张伟(总经理)"这种同时满足两个条件的行重复输出------不会,同一行只出现一次。
第 5 题
sql
SELECT ename, job FROM emp
WHERE comm IS NULL
ORDER BY ename;
思路:10 行,刚好和"有提成的 3 人"互补(13 − 3 = 10),可以拿来自查。 这道题唯一的陷阱就是别写 comm = NULL。
配套源码:本文示例数据自带(每段 SQL 可直接复制运行,不依赖任何外部库) | 练习题:第 2--5 题,答案见上
下一篇讲排序和分页。我会用一个你一定会遇到的现象开头:同一条分页 SQL,翻到第二页时数据发生了变化------ 原因不在分页,在排序。