Skip to content

02 · 检索、排序与条件过滤 ​

对应第 4~9 章。先完成练习数据准备,每次重新连接后执行 USE showdocs_mysql_book;。

本单元用一句具体需求贯穿查询:“从课程表中找出符合条件的课程,只返回需要的列,按指定顺序显示一页。”写 SQL 前先把需求拆成四件事:查哪张表、筛哪些行、显示哪些列、按什么顺序取多少条。混淆这四件事,是查询结果看似正确却不稳定的常见原因。

第 4 章:明确需要哪些列 ​

sql
SELECT course_id, title, price FROM courses ORDER BY course_id;
SELECT DISTINCT category FROM courses ORDER BY category;

第一条返回 4 门课程;第二条返回两个类别。DISTINCT 对整个选择列组合去重,增加 course_id 后通常就不再能去除重复类别。练习时可以用 SELECT * 查看数据,固定用途的查询宜明确列名。

从一个结果单元格追溯来源 ​

sql
SELECT course_id AS id, title AS course_title
FROM showdocs_mysql_book.courses
WHERE course_id = 101;

返回一行 101 / SQL 入门。FROM 指出数据来源,WHERE 选择课程 101,SELECT 取出两列,AS 只改结果列的名字,不会修改表字段。完全限定名由“库名.表名”组成;它可以在当前未选库时明确访问目标库中的表。

列之间用逗号分隔,最后一列后不能多写逗号。字符串常量用单引号,列名通常不加引号;SELECT 'title' FROM courses 会把常量 title 输出四次,而不是读取课程名。需要引用含特殊字符的标识符时使用反引号,但更好的建模方式是避免难以阅读的名称。

去重看的是选择出来的整行 ​

sql
SELECT DISTINCT learner_id FROM enrollments ORDER BY learner_id;
SELECT DISTINCT learner_id, course_id FROM enrollments ORDER BY learner_id, course_id;

第一条返回 1、2,第二条仍有三行 (1,101)、(1,102)、(2,101)。如果要回答“有多少不同学员”,不能把课程编号也选出来后期待 DISTINCT 只针对学员列。

SELECT * 会读取所有列,未来加列也会改变结果结构。它适合临时观察表,但固定接口或报表通常应明确列清单;这还能避免把长文本 summary 等本次不需要的数据传给客户端。

注释与可读性 ​

sql
-- MySQL 的双横线注释后保留空白
SELECT course_id, title /* 本次只查看展示字段 */
FROM courses
ORDER BY course_id;

换行和缩进通常不改变 SQL 含义,但能把数据源、条件和排序分开。不要把一大段查询粘成一行再排查错误;先单独验证能读到数据,再逐个加条件。

第 5 章:排序后再取前几条 ​

sql
SELECT course_id, title, price
FROM courses
ORDER BY price DESC, course_id ASC
LIMIT 2;

预期先返回 102,再返回 104。每个排序列的方向单独指定;唯一编号作为最后一个排序键,让价格相同时顺序仍然确定。没有 ORDER BY 时,不应依赖返回顺序;LIMIT 只限制返回数量。

多列排序是逐层比较 ​

ORDER BY category, price DESC 先按类别排序,只在类别相同的记录之间比较价格,不能理解为“所有记录按价格降序后再加一个类别标签”。DESC 只修饰它前面的排序表达式;若两列都需要降序,必须分别写出。

sql
SELECT course_id, title, price FROM courses
ORDER BY price DESC, course_id
LIMIT 2 OFFSET 1;

先得到价格降序序列 102、104、103、101,跳过 1 行,再取 2 行,所以返回 104、103。OFFSET 从 0 开始计算;MySQL 的 LIMIT 1, 2 与这里等价,但“数量加 OFFSET”的形式更不易把参数记反。

排序也可以写列位置,如 ORDER BY 3 DESC。它依赖 SELECT 列顺序,一旦插入新列就可能改变含义,所以长久维护的 SQL 更适合列名或别名。没有唯一排序键时,并列记录在不同执行计划下可能交换位置,分页可能因此重复或漏读。

即使排序键唯一,多个分页请求之间发生数据插入或删除,OFFSET 分页仍可能漂移。这属于翻页期间数据变化的问题,不是增加一个 ORDER BY 就能完全解决;后续可继续学习本站的游标分页。这里先保证静态数据上的顺序确定。

第 6 章:范围与空值 ​

sql
SELECT course_id, price FROM courses
WHERE price BETWEEN 39.00 AND 59.00 ORDER BY course_id;

SELECT learner_id, learner_name FROM learners WHERE email IS NULL;

BETWEEN 包含上下界,因此返回 101、103、104;空邮箱查询返回学员 2。NULL 不是空字符串,也不是 0,不能通过 email = NULL 判断。WHERE 只保留条件为真的行,未知的比较结果不会通过过滤。

比较运算符与数据类型 ​

写法含义本数据的例子
=相等price = 39 命中 101
<> 或 !=不相等price <> 39 命中 102、103、104
<、<=、>、>=大小比较price < 49 只命中 101
BETWEEN a AND b包含两端的闭区间BETWEEN 39 AND 49 命中 101、103
IS NULL / IS NOT NULL检查缺失值只有学员 2 的邮箱为空值

数字尽量与数字比较,日期用明确格式。MySQL 可能对不同类型做隐式转换,但能执行并不代表业务含义正确;例如把字符串列强制当数值比较,可能产生警告并影响索引使用。日期范围若将来改成 DATETIME,应优先使用“起点包含、终点不包含”,避免漏掉最后一天的非零时刻。

为什么“不等于某邮箱”没有返回空邮箱学员 ​

sql
SELECT learner_id, email FROM learners
WHERE email <> 'chen@example.test' ORDER BY learner_id;

SELECT learner_id, email FROM learners
WHERE email <> 'chen@example.test' OR email IS NULL ORDER BY learner_id;

第一条只返回周三;第二条返回林二、周三。对未知邮箱,数据库不能断言它等于或不等于给定地址,所以比较结果是 UNKNOWN,由 NULL 表示。WHERE 只接受 TRUE,FALSE 和 UNKNOWN 都会被丢弃。

sql
SELECT NULL = NULL AS ordinary_equal,
       NULL <> 1 AS ordinary_not_equal,
       NULL IS NULL AS is_missing,
       NULL <=> NULL AS null_safe_equal,
       '' IS NULL AS empty_is_missing;

预期依次是 NULL、NULL、1、1、0。MySQL 的 <=> 是 NULL 安全相等比较:两个 NULL 判为相等,一个 NULL 和一个非 NULL 判为不等。在筛选缺失值这个具体任务中,IS NULL 更直接。

第 7 章:用括号表达业务条件 ​

sql
SELECT course_id, title FROM courses
WHERE (category = '数据库' OR category = '运维') AND price < 60
ORDER BY course_id;

SELECT course_id FROM courses
WHERE category IN ('数据库', '运维') AND price < 60
ORDER BY course_id;

两条都返回 101、103、104。AND 的优先级高于 OR,去掉第一条的括号会让所有数据库课程都被选中。含 NULL 的 NOT IN 容易产生未知结果;需要表达「不存在匹配记录」时,下一单元的 NOT EXISTS 更容易说明意图。

用一行数据检查括号,而不是凭感觉读条件 ​

课程 102 的类别是数据库、价格是 79。对 category='数据库' OR category='运维' AND price<60,右侧 AND 先计算;左边“是数据库”已经为真,因此整条条件为真。若需求是“两种类别都必须低于 60”,就必须把类别条件括起来。

三值逻辑中最容易误判的是下面几种组合:

左值运算右值结果
TRUEANDUNKNOWNUNKNOWN
FALSEANDUNKNOWNFALSE
TRUEORUNKNOWNTRUE
FALSEORUNKNOWNUNKNOWN
UNKNOWNNOT—UNKNOWN

这张表解释了为什么 email IS NULL OR email <> ... 能保留缺失邮箱,而简单加 NOT 不能把 UNKNOWN 变成 TRUE。

NOT IN 的 NULL 陷阱可以直接观察 ​

sql
SELECT 103 IN (101, NULL) AS in_result,
       103 NOT IN (101, NULL) AS not_in_result,
       101 NOT IN (101, NULL) AS matched_result;

预期是 NULL、NULL、0。对于 103,既不等于 101,又无法判断它是否等于未知值,所以 IN 是 UNKNOWN,取反后仍然 UNKNOWN。若子查询返回了 NULL,WHERE x NOT IN (子查询) 就可能意外丢掉本来想保留的记录。可根据需求排除子查询中的 NULL,或用带清晰关联条件的 NOT EXISTS。

第 8 章:用通配符匹配文本 ​

sql
SELECT course_id, title FROM courses WHERE title LIKE 'SQL%' ORDER BY course_id;
SELECT course_id, title FROM courses WHERE title LIKE '%入门' ORDER BY course_id;

% 匹配零个或多个字符,_ 匹配一个字符。预期第一条返回 101、104,第二条返回 101、103。匹配大小写取决于字符集校对规则;以 % 开头的模式通常不能按普通 B-tree 索引进行前缀范围定位。

LIKE 匹配整个字符串模式 ​

LIKE 'SQL' 要求整个值符合 SQL,不会匹配 SQL 入门;LIKE 'SQL%' 才允许后面还有内容。_ 是一个字符,不是一个字节,因此用于 UTF-8 中文时也按字符匹配。

sql
SELECT 'SQL' LIKE 'SQL%' AS zero_extra,
       'SQL 入门' LIKE 'SQL___' AS three_extra,
       'SQL 入门' LIKE 'SQL__' AS two_extra,
       NULL LIKE '%' AS missing_text;

SELECT 'discount_10%' LIKE 'discount!_10!%' ESCAPE '!' AS literal_match;

第一组预期为 1、1、0、NULL,空格也算一个字符。第二条预期为 1:使用显式转义字符,把 _ 和 % 当作普通字符。用户搜索输入如果希望按原样匹配,也要处理这两个字符;参数化查询解决 SQL 注入,并不会自动改变 LIKE 通配符的含义。

模式越宽泛,匹配和扫描成本通常越高。先用类别、日期等明确条件缩小范围,再使用模式匹配,往往比对所有长文本做 %关键词% 更可控。是否能用索引仍需查看实际执行计划。

第 9 章:让正则表达式描述模式 ​

sql
SELECT course_id, title FROM courses
WHERE REGEXP_LIKE(title, '^(SQL|MySQL)', 'c')
ORDER BY course_id;

预期返回 101、102、104。^ 要求从字符串开头匹配,| 表示候选分支,'c' 显式要求大小写敏感。正则表达式与 LIKE 的通配符不是同一套语法;这里使用 MySQL 8 的 REGEXP_LIKE。

从简单模式逐步组合 ​

片段含义适合的用途
^、$字符串开头、结尾限制整个字段的形式
.单个字符,换行行为受选项影响一个未知字符位置
[0-9]、[[:digit:]]字符范围、字符类检查数字字符
(SQL|MySQL)选择分支两种可接受的前缀
*、+、?零或多次、一次以上、零或一次描述重复次数
{2,4}重复 2~4 次限制长度

正则默认查找字符串中的匹配片段。要验证整个字段,例如格式必须是 C 后跟三位数字,需要同时写 ^ 和 $:

sql
SELECT REGEXP_LIKE('C101', '^C[0-9]{3}$', 'c') AS valid_code,
       REGEXP_LIKE('XC101', '^C[0-9]{3}$', 'c') AS extra_prefix,
       REGEXP_LIKE('c101', '^C[0-9]{3}$', 'c') AS wrong_case;

预期为 1、0、0。LIKE 中的 % 不能直接搬到正则里表示任意字符;正则中的对应组合通常是 .*。涉及反斜杠时,还要考虑 SQL 字符串解析和正则解析两个层次,优先使用清晰的字符类,避免复制复杂模式后不知道实际传给正则引擎的内容。

练习与核对 ​

查找「数据库类别、价格不超过 60、课程名以 SQL 开头」的课程,按价格降序排列。自行组合上面的条件,预期顺序为 104、101。把价格上限改成 39,预期只剩 101。

参考解答与推导
sql
SELECT course_id, title, price FROM courses
WHERE category = '数据库' AND price <= 60 AND title LIKE 'SQL%'
ORDER BY price DESC, course_id;

先按类别排除 103,再按价格排除 102,最后保留名称以 SQL 开头的 101、104;价格降序使 104 在前。把条件写全比依赖当前数据“刚好只有这些课程”更重要。

练习二:列出已填写邮箱且不是陈一邮箱的学员。 预期只返回 3。参考条件为 email IS NOT NULL AND email <> 'chen@example.test';若需求改成“邮箱未知也保留”,应改用 OR,而不是把 IS NOT NULL 换成 IS NULL 后仍保留 AND。

练习三:每页两门课,按价格从低到高查看第二页。 先列出排序序列 101、103、104、102,所以第二页应为 104、102:

sql
SELECT course_id, price FROM courses
ORDER BY price, course_id LIMIT 2 OFFSET 2;

练习四:解释 WHERE email = NULL 为什么不是语法错误,却一行也查不到。 SQL 能正常计算这个表达式;问题在于计算结果不是 TRUE。这种“语句执行成功但语义错误”比语法错误更需要用已知数据检验。

官方 SELECT 语法 · 官方正则函数

返回导读 · 下一单元:计算与多表查询

继续阅读