大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
WHERE 过滤与排序
这一课你将能做到
- 用比较、逻辑、IN、BETWEEN、LIKE、IS NULL 组合出精确的过滤条件
- 用 ORDER BY 做单列/多列、升序/降序排序,并理解 NULL 在排序里的位置
- 用 LIMIT + OFFSET 实现分页,并知道深分页为什么会慢
前置:第一课的 sql_practice 库已就绪,本课语句都在这个库里执行。
为什么要学
上一课 SELECT 解决「要哪些列」,这一课 WHERE 解决「要哪些行」。真实业务里几乎没有「查全表」的接口:商品列表按价格区间筛、订单按状态筛、搜索框按关键词模糊匹配——全是 WHERE。排序和分页也是列表接口的标配,写成什么样直接决定查询走不走索引(第 8 课见分晓)。
先建立图像
WHERE 就是一张筛网:每一行被单独拎出来过一遍条件,条件为真(TRUE)的留下,为假(FALSE)的丢掉,算出来「不知道」(UNKNOWN)的也丢掉。
study_record 17 行
│ WHERE completed = 1
▼
16 行(大鹏那条 completed=0 被筛掉)
│ ORDER BY score DESC
▼
顺序重排
│ LIMIT 3
▼
只剩 3 行
关键:NULL 和任何值比较(包括 NULL = NULL)结果都是 UNKNOWN,不是 TRUE——这就是「为什么 = NULL 查不到东西」的根源。
跟着做
步骤 1:单条件比较
SELECT title, price FROM course WHERE price > 100;
-- 3 行:Java 基础入门 199 / Java 面向对象进阶 299 / MySQL 性能优化实战 399
SELECT title, status FROM course WHERE status = 'published';
-- 5 行
运算符:=、!=(等价于标准写法 <>)、>、>=、<、<=。字符串比较默认不区分大小写(取决于排序规则,本库是 utf8mb4_unicode_ci)。
步骤 2:多条件 AND / OR,括号保平安
-- AND:同时满足
SELECT title FROM course WHERE category = 'java' AND status = 'published';
-- 2 行
-- OR 不加括号的经典翻车:
SELECT title, category, level, price
FROM course
WHERE category = 'database' AND level = 'advanced' OR price = 0;
-- 得到 4 行!AND 优先级高于 OR,后半个条件把所有 0 元课都放进来了
-- 加括号才是原意:
SELECT title, category, level, price
FROM course
WHERE category = 'database' AND (level = 'advanced' OR price = 0);
-- 3 行:SQL 与关系型数据库 / MySQL 性能优化实战 / Redis 缓存入门
规则很简单:AND 和 OR 混用,永远加括号,写给自己也写给读代码的人。
步骤 3:IN / NOT IN —— 多值等值判断
SELECT name, city FROM student WHERE city IN ('深圳', '杭州');
-- 3 行:老陈、小林、小夏
SELECT title FROM course WHERE category NOT IN ('java');
-- 5 行
IN 等价于一串 = 用 OR 连接,但更短、更好优化。注意一个深坑:NOT IN (子查询) 时,子查询结果里只要有一个 NULL,整个 NOT IN 永远返回空——本课先记结论,JOIN 课教 NOT EXISTS 替代。
步骤 4:BETWEEN 区间,注意闭区间和日期边界
SELECT title, price FROM course WHERE price BETWEEN 50 AND 200;
-- 2 行:前端页面基础 99 / Java 基础入门 199(50 和 200 都包含在内)
日期区间的经典坑:'2026-06-10' 会被解析成 '2026-06-10 00:00:00',当天上午的记录全部漏掉:
SELECT title, created_at FROM course
WHERE created_at BETWEEN '2026-06-01' AND '2026-06-10';
-- 只有 4 行!course 5 是 06-10 10:00 创建的,被「00:00:00」边界吃掉了
-- 正确的半开区间写法(左闭右开):
SELECT title, created_at FROM course
WHERE created_at >= '2026-06-10 00:00:00'
AND created_at < '2026-06-11 00:00:00';
步骤 5:LIKE 模糊匹配
SELECT title FROM course WHERE title LIKE '%Java%'; -- 2 行(包含 Java)
SELECT slug FROM course WHERE slug LIKE 'java-%'; -- 2 行(前缀匹配)
SELECT slug FROM course WHERE slug LIKE 'java-_'; -- 1 行:java-oop(_ 占 1 个字符)
%匹配任意长度,_匹配单个字符LIKE 'java-%'前缀确定,可以走索引;LIKE '%Java%'前缀不确定,走不了(第 8 课用 EXPLAIN 验证)
步骤 6:IS NULL —— 判空唯一正确姿势
SELECT id, student_id, lesson_id, completed_at
FROM study_record
WHERE completed_at IS NULL;
-- 1 行:id=12,大鹏学了但没完成的那条
SELECT COUNT(*) FROM study_record WHERE completed_at IS NOT NULL; -- 16
WHERE completed_at = NULL 永远返回空集,因为 NULL 和任何值的比较都是 UNKNOWN。判空只能用 IS NULL / IS NOT NULL。
步骤 7:ORDER BY 排序
SELECT title, price FROM course
WHERE status = 'published'
ORDER BY price DESC; -- 降序:399 → 199
-- 多列排序:先按 category 升序,同分类内按 price 降序
SELECT category, title, price FROM course ORDER BY category ASC, price DESC;
-- ASC 是默认值,可省略
SELECT name, balance FROM student ORDER BY balance; -- 0 → 500
NULL 在排序里的位置(MySQL 规则):升序时 NULL 排最前面,降序时排最后面。想强制控制用 ORDER BY completed_at IS NULL, completed_at(先把 NULL 沉底)。
步骤 8:LIMIT / OFFSET 分页
SELECT id, name FROM student ORDER BY id LIMIT 3; -- 第 1 页:1,2,3
SELECT id, name FROM student ORDER BY id LIMIT 3 OFFSET 3; -- 第 2 页:4,5,6
-- LIMIT 3 = LIMIT 3 OFFSET 3 的简写
通用公式:第 n 页、每页 pageSize 条 → LIMIT pageSize OFFSET (n-1)*pageSize。前提是必须先 ORDER BY 一个稳定列(通常是主键或带索引的时间列),否则翻页会出现重复和漏行。
讲清楚要点
- 三值逻辑是 WHERE 的地基:TRUE / FALSE / UNKNOWN。只有 TRUE 的行留下;NULL 参与比较产生 UNKNOWN,行被丢弃。所有「查不到明明存在的数据」的问题,先怀疑 NULL。
- AND 优先级高于 OR:混用必须加括号,没有例外。
- IN 用于多值等值,BETWEEN 用于闭区间;日期比较用半开区间
>= 当天00:00 AND < 次日00:00最不容易漏。 - LIKE 的索引友好度:前缀匹配
LIKE 'x%'能走索引;中缀/后缀'%x%'、'%x'走不了。这决定了搜索功能该不该用 LIKE 实现。 - ORDER BY 可以用 SELECT 别名(它跑在 SELECT 之后),多列排序按书写顺序依次生效,每列可独立指定 ASC/DESC。
- 分页必须配合稳定排序;
OFFSET越大扫描丢弃越多,深分页是性能杀手——优化思路在第 8 课后你会更有体感,先记住「翻到很后面就慢」。
你可能会懵的地方
-
<>和!=用哪个? 等价。<>是 SQL 标准,跨数据库都能用;!=是 MySQL 等支持的简写。团队统一一种即可。 -
为什么
WHERE col != 5查不出 col 为 NULL 的行?NULL != 5结果是 UNKNOWN,不是 TRUE。要包含 NULL 行得显式写(col != 5 OR col IS NULL)。 -
0 和 NULL 一样吗? 完全不同。本练习库里
completed = 0表示「开始了没完成」,score = NULL表示「还没打分」。混为一谈,统计口径就错了。 -
LIMIT 不加 ORDER BY 会怎样? 返回哪三行不确定。看似每次都一样,换执行计划或并发后就漂移。分页永远带 ORDER BY。
-
WHERE 里能写聚合吗(WHERE COUNT(*) > 3)? 不能,聚合发生在分组阶段,WHERE 在它之前。这类条件下一课用 HAVING 写。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
WHERE col = NULL | UNKNOWN,永远空集 | col IS NULL |
| AND / OR 混用不加括号 | 条件组合与预期不符 | 每处混用都加括号 |
日期 BETWEEN '06-01' AND '06-10' | 吃掉 10 日全天 | >= '06-10 00:00:00' AND < '06-11 00:00:00' |
NOT IN (子查询) 遇 NULL | 永远空集 | NOT EXISTS(JOIN 课展开) |
| LIKE 中缀匹配当搜索用 | 全表扫描 | 前缀匹配或接入搜索引擎/全文索引 |
| 分页不带 ORDER BY | 翻页重复/漏行 | 先排序再 LIMIT |
卡点急救
You have an error in your SQL syntax:检查关键字顺序(WHERE 在 ORDER BY 之前)、字符串是否漏单引号、括号是否配对。Empty set但数据明明有:三步排查——值两边有没有不可见空格;是不是 NULL 比较;大小写/类型是否对上(字符串列传了数字会隐式转换)。- 查询巨慢:先
LIMIT 10验证逻辑对不对,再谈性能;把EXPLAIN放在语句最前面看走没走索引(第 8 课详解)。 - 排序结果「不稳定」:排序列有重复值(如多个 price 相同)。补一列唯一值做次级排序,如
ORDER BY price DESC, id。
面试常问
-
NULL 在 SQL 里怎么比较?为什么
= NULL不行? NULL 参与任何比较都返回 UNKNOWN(三值逻辑),只有IS NULL能判定。聚合函数(COUNT(col)、AVG)也会忽略 NULL,这是相关高频追问。 -
分页是怎么实现的?深分页为什么慢?
LIMIT n OFFSET m;OFFSET 越大,数据库要扫描并丢弃的行越多。优化方向:只翻有限页、用WHERE id > 上一页最大id LIMIT n的游标分页、或覆盖索引 + 延迟关联。 -
WHERE 和 HAVING 的区别? WHERE 过滤行(分组前,不能用聚合),HAVING 过滤组(分组后,可以聚合)。下一课展开。
-
字符串模糊搜索有什么方案? 前缀匹配走索引;任意位置匹配用全文索引(FULLTEXT)或外置搜索引擎(ES),LIKE '%x%' 在数据量大时不可接受。
自测三问
- 写条件:价格在 100~300 之间、或分类是 frontend 的已发布课程。
completed_at可能为 NULL 的列,怎么统计「已完成且有完成时间」的记录数?- 每页 10 条,第 5 页的 LIMIT/OFFSET 怎么写?
小结一句话
WHERE 用「比较 + 逻辑 + IN/BETWEEN/LIKE/IS NULL」挑行,ORDER BY + LIMIT 负责顺序和分页;NULL 永远用 IS NULL 判,AND/OR 混用永远加括号。
延伸
- MySQL 比较函数与运算符
- 下一课:JOIN 多表联结——数据拆在多张表里,怎么一次查回来。