SQL(Structured Query Language,结构化查询语言)是操纵关系型数据库的通用语言,而在 SQL 的众多语句里,查询(检索) 是出场率最高、也最能体现功底的部分。前面我们学会了建表、插数据,那都是为了"把数据放进去";查询则是"把数据拿出来、并且按你想要的样子拿出来"。很多时候,业务的问题本质就是一条查询语句——"这个月卖了多少"、"谁是冠军"、"哪些订单逾期了",这些全都能落地成一句 SELECT。
这篇文章我们就专心把"查询"这一件事掰开揉碎:从最基础的 SELECT 列选择、别名,到 WHERE 条件与模糊/区间查询,再到 DISTINCT 去重、排序 ORDER BY、分页 LIMIT,最后是聚合函数和分组 GROUP BY/HAVING。学完这一篇,你会真正理解一条查询语句的每一块零件各管什么、它们又是按什么顺序协同工作的——尤其是那几个最容易把人绊倒的坑:WHERE 里为什么不能用聚合、HAVING 跟 WHERE 到底差在哪、NULL 为什么总是"不听话"。
在动手之前,我们先把功课用的表准备好。我们创建一张考试结果表 exam_result,装进七位同学的三科成绩,后面的例子大多在它上面跑:
-- 创建考试成绩表
CREATE TABLE exam_result (
id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT, -- 主键,自增,一带一不可重复
name VARCHAR(20) NOT NULL COMMENT '同学姓名', -- 姓名,非空
chinese FLOAT DEFAULT 0.0 COMMENT '语文成绩', -- 语文成绩
math FLOAT DEFAULT 0.0 COMMENT '数学成绩', -- 数学成绩
english FLOAT DEFAULT 0.0 COMMENT '英语成绩' -- 英语成绩
);
-- 插入七条测试数据,id 会自动递增,不用手动给
INSERT INTO exam_result (name, chinese, math, english) VALUES
('唐三藏', 67, 98, 56),
('孙悟空', 87, 78, 77),
('猪悟能', 88, 98, 90),
('曹孟德', 82, 84, 67),
('刘玄德', 55, 85, 45),
('孙权', 70, 73, 78),
('宋公明', 75, 65, 30);先把这张表的庐山真面目看清楚。留意 id 自动从 1 排到 7:
SELECT * FROM exam_result; -- * 表示"选择所有的列"
-- 执行结果:
-- +----+-----------+---------+------+---------+
-- | id | name | chinese | math | english |
-- +----+-----------+---------+------+---------+
-- | 1 | 唐三藏 | 67 | 98 | 56 |
-- | 2 | 孙悟空 | 87 | 78 | 77 |
-- | 3 | 猪悟能 | 88 | 98 | 90 |
-- | 4 | 曹孟德 | 82 | 84 | 67 |
-- | 5 | 刘玄德 | 55 | 85 | 45 |
-- | 6 | 孙权 | 70 | 73 | 78 |
-- | 7 | 宋公明 | 75 | 65 | 30 |
-- +----+-----------+---------+------+---------+
-- 7 rows in set (0.00 sec)SELECT 列选择:你想看哪些列
SELECT 翻译过来就是"选择",它的第一个作用就是挑列:你只想要哪几列,就把它们列在 SELECT 后面。
全列查询与它的代价
上面那句 SELECT *(星号代表全部列)就是全列查询。这对新手很友好,但在真实项目里 * 其实是个"嫌贫爱富"的懒人写法,通常不推荐,原因有两个:
- 查询的列越多,需要从服务器传到客户端的数据量就越大。表有 100 个字段你却只要 1 个,白白多搬了 99 列。
- 有可能影响索引的使用(索引能让查询变快,后面讲索引时会展开,这里先记住结论)。
指定列查询
把需要的那几列的名字写出来就行,顺序可以随意,不必跟建表时的顺序一致:
-- 只查询 id、姓名、英语成绩三列
SELECT id, name, english FROM exam_result;
-- 执行结果:
-- +----+-----------+---------+
-- | id | name | english |
-- +----+-----------+---------+
-- | 1 | 唐三藏 | 56 |
-- | 2 | 孙悟空 | 77 |
-- | 3 | 猪悟能 | 90 |
-- | 4 | 曹孟德 | 67 |
-- | 5 | 刘玄德 | 45 |
-- | 6 | 孙权 | 78 |
-- | 7 | 宋公明 | 30 |
-- +----+-----------+---------+
-- 7 rows in set (0.00 sec)用表达式查询:SELECT 不只是一张"复读机"
SELECT 后面不仅能放列名,还能放表达式。所谓表达式,就是由列、常量、加减乘除运算组合出来的东西。MySQL 会对每一行都去算一遍这个表达式,再把结果输出成新的一列。
先看一个不掺任何列的纯常量:
-- 输出一个常量 10,对每一行结果都打印一遍 10,跟 math 无关
SELECT id, name, 10 FROM exam_result;
-- 执行结果(共 7 行,每行第三列都是 10):
-- +----+-----------+----+
-- | id | name | 10 |
-- +----+-----------+----+
-- | 1 | 唐三藏 | 10 |
-- | 2 | 孙悟空 | 10 |
-- | ...(其余行同样) |
-- | 7 | 宋公明 | 10 |
-- +----+-----------+----+
-- 7 rows in set (0.00 sec)再看包含字段的表达式——把英语成绩统一加 10 分:
-- 对每一行,都计算 english + 10 并作为一列输出
SELECT id, name, english + 10 FROM exam_result;
-- 执行结果:
-- +----+-----------+------------+
-- | id | name | english+10 |
-- +----+-----------+------------+
-- | 1 | 唐三藏 | 66 |
-- | 2 | 孙悟空 | 87 |
-- | 3 | 猪悟能 | 100 |
-- | 4 | 曹孟德 | 77 |
-- | 5 | 刘玄德 | 55 |
-- | 6 | 孙权 | 88 |
-- | 7 | 宋公明 | 40 |
-- +----+-----------+------------+
-- 7 rows in set (0.00 sec)多个字段一起参与运算,最常见的就是算总分:
-- 把三科成绩加起来,就是每个同学的总分
SELECT id, name, chinese + math + english FROM exam_result;
-- 执行结果:
-- +----+-----------+------------------------+
-- | id | name | chinese+math+english |
-- +----+-----------+------------------------+
-- | 1 | 唐三藏 | 221 |
-- | 2 | 孙悟空 | 242 |
-- | 3 | 猪悟能 | 276 |
-- | 4 | 曹孟德 | 233 |
-- | 5 | 刘玄德 | 185 |
-- | 6 | 孙权 | 221 |
-- | 7 | 宋公明 | 170 |
-- +----+-----------+------------------------+
-- 7 rows in set (0.00 sec)这里你会发现,MySQL 给表达式那列起的列名就是表达式本身(chinese+math+english),又长又难看。我们要给它换个名字。
别名:让列名变得可读
SELECT 支持用 AS 给一个列(或一个表达式)起别名,AS 关键字可以省略。它的作用就是让输出结果的表头更加可读、语义化。
-- 用 AS 给总分表达式起别名"总分",起别名后表头就不再是一长串表达式了
SELECT id, name, chinese + math + english AS 总分 FROM exam_result;
-- 输出表头第一行是"总分",结果如下(AS 也可以不写):
-- +----+-----------+--------+
-- | id | name | 总分 |
-- +----+-----------+--------+
-- | 1 | 唐三藏 | 221 |
-- | 2 | 孙悟空 | 242 |
-- | 3 | 猪悟能 | 276 |
-- | 4 | 曹孟德 | 233 |
-- | 5 | 刘玄德 | 185 |
-- | 6 | 孙权 | 221 |
-- | 7 | 宋公明 | 170 |
-- +----+-----------+--------+
-- 7 rows in set (0.00 sec)AS 是可省略的,SELECT id, name, chinese + math + english 总分 ... 等价于上面这句。别名大多是英文(为了避开引号问题),但这里顺手演示一下中文别名也没问题。
这里必须提前给你打一针,因为完全没关系却最容易想当然:别名不能用到 WHERE 里。为什么?因为 MySQL 的执行顺序是"先 FROM 找表,再 WHERE 过滤行,最后才 SELECT 挑列起别名"。WHERE 判断的时候,SELECT 阶段还没执行,别名自然还不存在。这一点我们在 WHERE 一节还会郑重地再讲一遍。
DISTINCT:把重复的行干掉
DISTINCT 的意思是一词以蔽之——去重。它作用于 SELECT 出来的整行(如果选了多列,就是这几列的组合),把完全相同的行合并成一行。
先看不去重时,数学成绩里 98 是重复的:
-- 数学成绩里,98 出现两次
SELECT math FROM exam_result;
-- 执行结果:
-- +------+
-- | math |
-- +------+
-- | 98 |
-- | 78 |
-- | 98 |
-- | 84 |
-- | 85 |
-- | 73 |
-- | 65 |
-- +------+加上 DISTINCT 之后,重复的 98 只剩一个:
-- DISTINCT 让重复值只保留一份
SELECT DISTINCT math FROM exam_result;
-- 执行结果:
-- +------+
-- | math |
-- +------+
-- | 98 |
-- | 78 |
-- | 84 |
-- | 85 |
-- | 73 |
-- | 65 |
-- +------+
-- 6 rows in set (0.00 sec)多列 DISTINCT 时按"组合"去重。比如 DISTINCT id, name 是按 (id, name) 这个二元组判断是否相同,只要 id 或 name 有任何一个不同,就算不同的行。
WHERE 条件:筛选出你想要的行
SELECT 负责"挑列",WHERE(翻译为"在哪/哪里",这里表示过滤条件)负责挑行——它从表里一行行检查,只留下满足条件的行。WHERE 后面跟的是能对每一行求值出对错的一个条件表达式。
WHERE 所用的运算符分两类:比较运算符和逻辑运算符。
先说比较运算符,我列全你记住:
| 运算符 | 说明 |
|---|---|
>, >=, <, <= | 大于、大于等于、小于、小于等于 |
= | 等于。对 NULL 不安全,比如 NULL = NULL 结果是 NULL(未知),不是真 |
<=> | 等于。NULL 安全,比如 NULL <=> NULL 结果是 1(真) |
!=, <> | 不等于(两个写法等价) |
BETWEEN a0 AND a1 | 区间匹配,闭区间 [a0, a1],只要 a0 <= 值 <= a1 就返回真 |
IN (值, ...) | 只要等于序列里的任意一个,就返回真 |
IS NULL | 判断是否为空值 |
IS NOT NULL | 判断是否为非空值 |
LIKE | 模糊匹配。% 表示任意多个(含 0 个)任意字符,_ 表示任意一个字符 |
再说逻辑运算符,用来把多个比较条件拼起来:
| 运算符 | 说明 |
|---|---|
AND | 多个条件必须全部为真,结果才为真(且) |
OR | 任意一个条件为真,结果就是真(或) |
NOT | 取反:条件为真结果为假,条件为假结果为真(非) |
基本比较:英语不及格
-- 英语成绩小于 60 的同学及成绩
SELECT name, english FROM exam_result WHERE english < 60;
-- 执行结果:
-- +-----------+---------+
-- | name | english |
-- +-----------+---------+
-- | 唐三藏 | 56 |
-- | 刘玄德 | 45 |
-- | 宋公明 | 30 |
-- +-----------+---------+
-- 3 rows in set (0.00 sec)区间匹配:语文成绩在 [80, 90]
两种等价写法,一种用 AND 手动限定上下界,一种用 BETWEEN:
-- 写法一:AND 把两个条件都写清楚,都得满足
SELECT name, chinese FROM exam_result
WHERE chinese >= 80 AND chinese <= 90;
-- 写法二:BETWEEN ... AND ... 表示闭区间 [80, 90],效果和上面完全一样
SELECT name, chinese FROM exam_result
WHERE chinese BETWEEN 80 AND 90;
-- 两种写法的结果都一样:
-- +-----------+--------+
-- | name | chinese|
-- +-----------+--------+
-- | 孙悟空 | 87 |
-- | 猪悟能 | 88 |
-- | 曹孟德 | 82 |
-- +-----------+--------+
-- 3 rows in set (0.00 sec)注意 BETWEEN 80 AND 90 是闭区间,包含 80 和 90 本身。
IN:命中一堆离散值里的任意一个
-- 数学成绩是 58、59、98、99 中任何一个的同学
-- 写法一:OR 一个一个列,又长又累
SELECT name, math FROM exam_result
WHERE math = 58 OR math = 59 OR math = 98 OR math = 99;
-- 写法二:IN 一口气把候选值装进小括号
SELECT name, math FROM exam_result WHERE math IN (58, 59, 98, 99);
-- 两种写法结果一样:
-- +-----------+------+
-- | name | math |
-- +-----------+------+
-- | 唐三藏 | 98 |
-- | 猪悟能 | 98 |
-- +-----------+------+
-- 2 rows in set (0.00 sec)模糊查询 LIKE:% 与 _ 的区别
LIKE 是最常用的模糊匹配。它的两个通配符要分清楚:
%:匹配**任意多个(包括 0 个)**任意字符。相当于"这一截随便是什么都行,甚至什么都没有"。_:匹配恰好一个任意字符。严格占一个坑。
来看"姓孙的同学"和"孙某(正好俩字)同学"的差别:
-- % 表示姓孙的:孙后面随便跟多少字符都行,"孙悟空"、"孙权"都算
SELECT name FROM exam_result WHERE name LIKE '孙%';
-- 执行结果:
-- +-----------+
-- | name |
-- +-----------+
-- | 孙悟空 |
-- | 孙权 |
-- +-----------+
-- 2 rows in set (0.00 sec)
-- _ 表示"孙"后面严格跟着"一个"字符:只能是"孙权"这种两字名
SELECT name FROM exam_result WHERE name LIKE '孙_';
-- 执行结果:
-- +--------+
-- | name |
-- +--------+
-- | 孙权 |
-- +--------+
-- 1 row in set (0.00 sec)"孙悟空"有三个字,孙_ 只允许"孙"加一个字,所以匹配不上;孙% 允许后面任意多个(这里是两个字"悟空")就能匹配上。这就是 % 和 _ 的差别——一个是"任意长度",一个是"精确一位"。
字段对字段比较
WHERE 里比较运算符两侧都可以是字段,意思是"把这一行里两个字段拿出来比一比":
-- 找出语文成绩比英语成绩好的同学
SELECT name, chinese, english FROM exam_result WHERE chinese > english;
-- 执行结果:
-- +-----------+---------+---------+
-- | name | chinese | english |
-- +-----------+---------+---------+
-- | 唐三藏 | 67 | 56 |
-- | 孙悟空 | 87 | 77 |
-- | 曹孟德 | 82 | 67 |
-- | 刘玄德 | 55 | 45 |
-- | 宋公明 | 75 | 30 |
-- +-----------+---------+---------+
-- 5 rows in set (0.00 sec)表达式与"别名不能用于 WHERE"
WHERE 里能用表达式,但能用表达式不等于能用"这个表达式的别名"。看下面对照——用 chinese + math + english 这个完整表达式没问题,但你要是想偷懒写 WHERE 总分 < 200 就会报错:
-- 总分在 200 分以下的同学
-- 注意:SELECT 里给表达式起了别名"总分",但 WHERE 里不能用这个别名!
SELECT name, chinese + math + english AS 总分 FROM exam_result
WHERE chinese + math + english < 200;
-- 执行结果:
-- +-----------+--------+
-- | name | 总分 |
-- +-----------+--------+
-- | 刘玄德 | 185 |
-- | 宋公明 | 170 |
-- +-----------+--------+
-- 2 rows in set (0.00 sec)如果你把 WHERE 写成 WHERE 总分 < 200,MySQL 会毫不留情地报"未知的列 '总分'"。这就是前面提到的:执行顺序上 WHERE 先于 SELECT 的起别名阶段,别名在 WHERE 里根本没出生。记住这条铁规:别名可以用在 ORDER BY、HAVING 里,就是不能用在 WHERE 里。
AND / OR / NOT 的组合
把逻辑运算符组合起来,就能表达"既要又要还要"式的复杂条件:
-- 语文成绩 > 80 并且不姓孙的同学
SELECT name, chinese FROM exam_result
WHERE chinese > 80 AND name NOT LIKE '孙%';
-- 执行结果:
-- +-----------+--------+
-- | name | chinese|
-- +-----------+--------+
-- | 猪悟能 | 88 |
-- | 曹孟德 | 82 |
-- +-----------+--------+
-- 2 rows in set (0.00 sec)再上一个综合性最强的小题,顺便训练一下千里眼般的括号阅读。题意:找出"孙某同学(两个字)",否则(也就是说其他情况下)要求总成绩 > 200 且语文成绩 < 数学成绩且英语成绩 > 80:
-- NOT LIKE '孙_' 与 OR 的配合:要么是两个字姓孙的,要么满足括号里三个硬条件
SELECT name, chinese, math, english, chinese + math + english AS 总分
FROM exam_result
WHERE name LIKE '孙_' OR (
chinese + math + english > 200
AND chinese < math
AND english > 80
);
-- 执行结果:
-- +-----------+--------+------+---------+--------+
-- | name | chinese| math | english | 总分 |
-- +-----------+--------+------+---------+--------+
-- | 猪悟能 | 88 | 98 | 90 | 276 |
-- | 孙权 | 70 | 73 | 78 | 221 |
-- +-----------+--------+------+---------+--------+
-- 2 rows in set (0.00 sec)逐个对一下:孙权正好是"孙某",中招;猪悟能不姓孙,只能走右边括号——总分 276 > 200、语文 88 < 数学 98、英语 90 > 80,三条全满足,也中招。注意括号很关键,它保证了"OR 或的是整个括号",而不是只有第一行。
关于运算符优先级
MySQL 里逻辑运算符的优先级从高到低大致是 NOT > AND > OR(括号 () 优先级最高,恩准强制改变顺序)。当写复杂条件时,最稳妥的姿势是用括号把意图框死,不要赌记忆里的优先级——宁可多写一对括号,也别让读代码的人猜。
NULL 的筛选:它为什么那么"不听话"
这是本课最反直觉、最值得反复强调的一节。NULL 在 MySQL 里代表"没有值、未知",它不是 0,也不是空字符串。 因此:
- 它跟任何值(包括它自己)做
=、!=、>这类普通比较,结果都是 NULL(未知)——NULL = NULL不是真也不是假,而是"未知"。这导致不能用= NULL去筛选 NULL,因为判断不出来。 - 专门用你的必须是
IS NULL和IS NOT NULL。
先准备场景——students 表里有些同学没有留 QQ,字段就是 NULL:
-- students 表当前状态:
-- +-----+-------+-----------+-------+
-- | id | sn | name | qq |
-- +-----+-------+-----------+-------+
-- | 100 | 10010 | 唐大师 | NULL |
-- | 101 | 10001 | 孙悟空 | 11111 |
-- | 103 | 20002 | 孙仲谋 | NULL |
-- | 104 | 20001 | 曹阿瞒 | NULL |
-- +-----+-------+-----------+-------+想知道哪些同学把 QQ 留下了,你得写 IS NOT NULL:
-- 找出 QQ 号已知(非 NULL)的同学
SELECT name, qq FROM students WHERE qq IS NOT NULL;
-- 执行结果:
-- +-----------+-------+
-- | name | qq |
-- +-----------+-------+
-- | 孙悟空 | 11111 |
-- +-----------+-------+
-- 1 row in set (0.00 sec)你要是气不过,用 WHERE qq = NULL 或 WHERE qq != NULL 去试,结果会是空集——因为 NULL 跟 NULL 比较是"未知",MySQL 不会认为它满足 = 也不认为它不满足 !=,于是干脆一行为都不给。
我们再从"原理层面"看清楚 = 和 <=> 的差别,这是无数面试题的常客:
-- = 对 NULL 不安全:NULL 和谁比都是 NULL(未知)
SELECT NULL = NULL, NULL = 1, NULL = 0;
-- 执行结果:
-- +-------------+----------+----------+
-- | NULL = NULL | NULL = 1 | NULL = 0 |
-- +-------------+----------+----------+
-- | NULL | NULL | NULL |
-- +-------------+----------+----------+
-- 1 row in set (0.00 sec)
-- <=> 是 NULL 安全的等值比较:两个 NULL 视为相等,结果是 1(真)
SELECT NULL <=> NULL, NULL <=> 1, NULL <=> 0;
-- 执行结果:
-- +---------------+------------+------------+
-- | NULL <=> NULL | NULL <=> 1 | NULL <=> 0 |
-- +---------------+------------+------------+
-- | 1 | 0 | 0 |
-- +---------------+------------+------------+
-- 1 row in set (0.00 sec)结论总结成一句话:常规比较里 NULL 就是"黑洞",碰谁谁"未知";想判空,一律用 IS NULL / IS NOT NULL;<=> 是当你想"把两个 NULL 也算相等"时才用的。
ORDER BY 排序:让结果井然有序
数据库里的行默认是没有固定顺序的。ORDER BY 的作用就是把结果按某列(或表达式)排好序。两个方向:ASC(ascending,升序,从小到大)和 DESC(descending,降序,从大到小),不写默认为 ASC。
先敲响一个警钟,这句话在书里反复出现:没有 ORDER BY 的查询,返回顺序是"未定义"的,永远不要依赖这个顺序。 数据库随时可能按不喜欢你的方式给你排结果。
-- 按数学成绩升序(从小到大)排序
SELECT name, math FROM exam_result ORDER BY math;
-- 执行结果:
-- +-----------+------+
-- | name | math |
-- +-----------+------+
-- | 宋公明 | 65 |
-- | 孙权 | 73 |
-- | 孙悟空 | 78 |
-- | 曹孟德 | 84 |
-- | 刘玄德 | 85 |
-- | 唐三藏 | 98 |
-- | 猪悟能 | 98 |
-- +-----------+------+NULL 在排序里的地位
排序时 NULL 被 MySQL 视为比任何值都小。于是升序时 NULL 排最前,降序时 NULL 排最后:
-- 按 qq 号升序:NULL 最小,排在最上面
SELECT name, qq FROM students ORDER BY qq;
-- 执行结果:
-- +-----------+-------+
-- | name | qq |
-- +-----------+-------+
-- | 唐大师 | NULL |
-- | 孙仲谋 | NULL |
-- | 曹阿瞒 | NULL |
-- | 孙悟空 | 11111 |
-- +-----------+-------+
-- 按 qq 号降序:NULL 仍是"最小",落到最下面
SELECT name, qq FROM students ORDER BY qq DESC;
-- 执行结果:
-- +-----------+-------+
-- | name | qq |
-- +-----------+-------+
-- | 孙悟空 | 11111 |
-- | 唐大师 | NULL |
-- | 孙仲谋 | NULL |
-- | 曹阿瞒 | NULL |
-- +-----------+-------+多字段排序:优先级随书写顺序
可以按多列排序,排第一列优先,第一列相等再看第二列,依此类推。这里我们要求:先按数学降序,数学一样的再按英语升序,英语再一样的再按语文升序:
-- 依次按 math 降序、english 升序、chinese 升序
SELECT name, math, english, chinese FROM exam_result
ORDER BY math DESC, english, chinese;
-- 执行结果:
-- +-----------+------+---------+--------+
-- | name | math | english | chinese|
-- +-----------+------+---------+--------+
-- | 唐三藏 | 98 | 56 | 67 |
-- | 猪悟能 | 98 | 90 | 88 |
-- | 刘玄德 | 85 | 45 | 55 |
-- | 曹孟德 | 84 | 67 | 82 |
-- | 孙悟空 | 78 | 77 | 87 |
-- | 孙权 | 73 | 78 | 70 |
-- | 宋公明 | 65 | 30 | 75 |
-- +-----------+------+---------+--------+注意看唐三藏和猪悟能:数学都是 98(并列第一),就轮到第二关键字英语——唐三藏 56 小于猪悟能 90,所以唐三藏靠前。每个关键字后面的 ASC/DESC 是各自独立的,ORDER BY math DESC, english 表示数学降序、英语升序(不写默认升序)。
ORDER BY 能用表达式和别名
排序发生在 SELECT 之后,所以ORDER BY 既可以用表达式,也能用列别名(这点跟 WHERE 正好相反):
-- 方式一:ORDER BY 直接放表达式
SELECT name, chinese + english + math FROM exam_result
ORDER BY chinese + english + math DESC;
-- 方式二:先起别名,ORDER BY 用别名,效果一样
SELECT name, chinese + english + math AS 总分 FROM exam_result
ORDER BY 总分 DESC;
-- 两种方式的执行结果:
-- +-----------+------------------------+
-- | name | chinese+english+math / |
-- ...
-- +-----------+------------------------+
-- | 猪悟能 | 276 |
-- | 孙悟空 | 242 |
-- | 曹孟德 | 233 |
-- | 唐三藏 | 221 |
-- | 孙权 | 221 |
-- | 刘玄德 | 185 |
-- | 宋公明 | 170 |
-- +-----------+------------------------+WHERE 和 ORDER BY 连用
它俩顺序就是书写顺序:先 WHERE 过滤,再 ORDER BY 排序。
-- 先筛出姓孙或姓曹的同学,再按数学成绩从高到低排
SELECT name, math FROM exam_result
WHERE name LIKE '孙%' OR name LIKE '曹%'
ORDER BY math DESC;
-- 执行结果:
-- +-----------+------+
-- | name | math |
-- +-----------+------+
-- | 曹孟德 | 84 |
-- | 孙悟空 | 78 |
-- | 孙权 | 73 |
-- +-----------+------+
-- 3 rows in set (0.00 sec)LIMIT 分页:只取一截结果
LIMIT 用来限制返回的行数,是做分页最重要的法宝。它有几种写法,你得都认识:
LIMIT s, n:从下标 s 开始,取 n 条。起始下标从 0 开始。LIMIT n:等价于LIMIT 0, n,即从第 0 条开始取 n 条。LIMIT n OFFSET s:从下标 s 开始取 n 条。比LIMIT s, n语义更直白,建议用这种。
分页公式先记住:要显示第 K 页(页号从 1 开始),每页 N 条,则 OFFSET 要写成 (K-1)*N。我们来按 id 做"每页 3 条"的三页:
-- 第 1 页:从 0 开始取 3 条
SELECT id, name, math, english, chinese FROM exam_result
ORDER BY id LIMIT 3 OFFSET 0;
-- 执行结果:id = 1,2,3 三行(唐三藏、孙悟空、猪悟能)
-- 第 2 页:从 3 开始取 3 条,即跳过前 3 条
SELECT id, name, math, english, chinese FROM exam_result
ORDER BY id LIMIT 3 OFFSET 3;
-- 执行结果:id = 4,5,6 三行(曹孟德、刘玄德、孙权)
-- 第 3 页:从 6 开始取 3 条,但只剩 1 条了,MySQL 不会报错,给多少出多少
SELECT id, name, math, english, chinese FROM exam_result
ORDER BY id LIMIT 3 OFFSET 6;
-- 执行结果:id = 7 一行(宋公明)这里有两个细节值得拎出来说。第一,分页几乎总是搭配 ORDER BY 一起用——因为如果没有固定顺序,"第几页"就毫无意义了(这一页给谁下一页也给谁)。你写 ORDER BY id 就是给每行一个稳定的先后位置,分页才靠谱。第二,最后一页数据不足不会报错,给几条是几条。
另外送来一句工程老司机的忠告:对未知表、大表做探索性查询时,最好先加一句 LIMIT 1。因为你根本不知道这张表是几十行还是几千万行,要是没加 LIMIT,一条 SELECT * 可能把整个库的数据全捞出来传输,把数据库拖到卡死。先 LIMIT 1 看个风水,确认表有货再继续。
聚合函数:把一批行"捏"成一个数
所谓聚合函数,英文叫 aggregate function,意思是它把多行数据汇总成一个结果。它处理的不是一行,而是一整批满足条件的行,最后吐出一个数。最常见的五个我要逐个给你过:
| 函数 | 说明 |
|---|---|
COUNT(expr) | 统计行数(数量) |
SUM(expr) | 求总和;非数值型对它没有意义 |
AVG(expr) | 求平均值;非数值型没有意义 |
MAX(expr) | 求最大值 |
MIN(expr) | 求最小值 |
(上表里 expr 除了列名,还可以加 DISTINCT,如 COUNT(DISTINCT math),表示"先去掉重复值再统计"。)
COUNT:统计数量,注意 * 与列名的区别
COUNT(*) 数的是"行数",一行的某个字段是 NULL 也不影响计数;而 COUNT(某一列) 数的是"该列非 NULL 的个数",NULL 会被跳过。这个区别在下文 QQ 例子里至关重要。
-- 全班一共多少同学:COUNT(*) 数全部行,不受 NULL 影响
SELECT COUNT(*) FROM students;
-- 执行结果:
-- +----------+
-- | COUNT(*) |
-- +----------+
-- | 4 |
-- +----------+
-- COUNT(1) 效果同 COUNT(*):1 是常量,每一行都非空,所以也等于总行数
SELECT COUNT(1) FROM students;
-- 执行结果:
-- +----------+
-- | COUNT(1) |
-- +----------+
-- | 4 |
-- +----------+
-- 统计真正留下了 QQ 号的同学:COUNT(qq) 会跳过 NULL
-- students 表 4 行里只有孙悟空有 qq,其余全是 NULL
SELECT COUNT(qq) FROM students;
-- 执行结果:
-- +-----------+
-- | COUNT(qq) |
-- +-----------+
-- | 1 |
-- +-----------+看到区别了吗?COUNT(*) 数出来 4(所有行),COUNT(qq) 数出来 1(只有一行 qq 非空)。要数"这一列有多少个非空值",用 COUNT(列名);要数"一共有多少行",用 COUNT(*) 或 COUNT(1)。
COUNT 也支持 DISTINCT:
-- COUNT(math):统计所有数学成绩的条数(不去重)
SELECT COUNT(math) FROM exam_result;
-- 执行结果:7(7 个学生每人一条)
-- COUNT(DISTINCT math):统计"不同分数"有几种
SELECT COUNT(DISTINCT math) FROM exam_result;
-- 执行结果:6(98 出现了两次,去重后只剩 6 个不同的分数 )SUM:求和与"空结果返回 NULL"的坑
-- 数学成绩的总分
SELECT SUM(math) FROM exam_result;
-- 执行结果:
-- +----------+
-- | SUM(math)|
-- +----------+
-- | 581 |
-- +----------+
-- 注意这个坑:没有任何行满足条件时,SUM 返回的不是 0,而是 NULL
-- 因为没有任何 < 60 的数学成绩,求和没有"对象"
SELECT SUM(math) FROM exam_result WHERE math < 60;
-- 执行结果:
-- +----------+
-- | SUM(math)|
-- +----------+
-- | NULL |
-- +----------+划重点:SUM 对空集返回 NULL 而非 0。这很容易在业务里踩雷——你以为算出来是 0,代码里一接却是个 NULL,于是加法崩了。空集时到底该显示 0 还是 NULL,得看业务语义,处理时可以用 IFNULL(SUM(...), 0) 来兜底(IFNULL 是一个让 NULL 变默认值的函数,后面讲函数时会详谈)。
AVG:平均值
-- 全班平均总分
SELECT AVG(chinese + math + english) AS 平均总分 FROM exam_result;
-- 执行结果:
-- +--------------+
-- | 平均总分 |
-- +--------------+
-- | 221.142857 |
-- +--------------+MAX 与 MIN:极值,且能配合 WHERE
-- 英语最高分
SELECT MAX(english) FROM exam_result;
-- 执行结果:90
-- 数学成绩 > 70 的那批人里的最低数学分
SELECT MIN(math) FROM exam_result WHERE math > 70;
-- 执行结果:73这里要敲最后一个雷,而且这雷放哪儿都会响:聚合函数不能出现在 WHERE 里。 你想"找出数学成绩超过平均水平的人"很容易顺手写成 WHERE math > AVG(math),MySQL 直接报错。为什么?因为 WHERE 是"在行还没分好、还没汇总时"逐行过滤用的;而聚合函数要"先把一堆行汇总成一个数",这属于分组/汇总阶段的事,等不到那一阶段 WHERE 早就过滤完了。想按聚合结果筛选,得用我们下面要讲的 HAVING。
GROUP BY 分组:把同类行"归堆"
GROUP BY(group by 可以拆开读:group 表示"分组",BY 表示"按什么")的作用很形象——把行按某个(些)列的值"归堆",值一样的被分到同一个组里,每个组只输出一行结果。它几乎总是和聚合函数配对出现:先分组,再对每组做聚合统计。
先准备一张演示分组的员工表。它来自 Oracle 9i 的经典测试表,我们摘个精简版,字段有部门 deptno、岗位 job、工资 sal:
-- 创建员工表 emp
CREATE TABLE emp (
empno INT PRIMARY KEY, -- 员工编号,主键
ename VARCHAR(20), -- 员工姓名
job VARCHAR(20), -- 岗位
deptno INT, -- 部门编号
sal DECIMAL(10, 2) -- 工资,带两位小数
);
-- 插入测试数据:覆盖 3 个部门、若干种岗位
INSERT INTO emp (empno, ename, job, deptno, sal) VALUES
(1, '唐三藏', '教师', 10, 3000),
(2, '孙悟空', '助教', 10, 2500),
(3, '猪悟能', '助教', 20, 2500),
(4, '曹孟德', '教师', 20, 3200),
(5, '刘玄德', '出纳', 30, 1500),
(6, '孙权', '会计', 30, 1800),
(7, '宋公明', '会计', 30, 2000);第一个经典需求:显示每个部门的平均工资和最高工资。按 deptno 分组,再对每组求 AVG 和 MAX:
-- 按部门分组,对每个部门分别求平均工资、最高工资
SELECT deptno, AVG(sal), MAX(sal) FROM emp GROUP BY deptno;
-- 执行结果:
-- +--------+------------+----------+
-- | deptno | AVG(sal) | MAX(sal) |
-- +--------+------------+----------+
-- | 10 | 2750.00 | 3000.00 |
-- | 20 | 2850.00 | 3200.00 |
-- | 30 | 1766.67 | 2000.00 |
-- +--------+------------+----------+对三组数据心算验证:部门 10 是教师 3000 + 助教 2500,平均 (3000+2500)/2 = 2750,最高 3000,完全对得上。
第二个需求:显示每个部门、每种岗位的平均工资和最低工资。这时要按两个字段分组 GROUP BY deptno, job,意思是"部门相同且岗位也相同的行算一组":
-- 先按部门分,部门里再按岗位分
SELECT deptno, job, AVG(sal), MIN(sal)
FROM emp
GROUP BY deptno, job;
-- 执行结果:
-- +--------+--------+------------+----------+
-- | deptno | job | AVG(sal) | MIN(sal) |
-- +--------+--------+------------+----------+
-- | 10 | 教师 | 3000.00 | 3000.00 |
-- | 10 | 助教 | 2500.00 | 2500.00 |
-- | 20 | 教师 | 3200.00 | 3200.00 |
-- | 20 | 助教 | 2500.00 | 2500.00 |
-- | 30 | 出纳 | 1500.00 | 1500.00 |
-- | 30 | 会计 | 1900.00 | 1800.00 |
-- +--------+--------+------------+----------+部门 30 的"会计"岗有两人(孙权 1800、宋公明 2000),所以平均 (1800+2000)/2=1900、最低 1800,正好体现"一组多行聚合成一行输出"。
分组后的 SELECT 列限制:一个必须讲清的规矩
这里有个很多初学者栽跟头、面试也爱问的点:GROUP BY 之后,SELECT 里能出现哪些列?
规矩(按标准 SQL 和 MySQL 5.7.5 之后默认开启的 ONLY_FULL_GROUP_BY 模式)是这样的:分组后,SELECT 里的列必须要么是"被用来分组的列",要么是"聚合函数包裹的列"。换句话说:
- 能写
GROUP BY里的列(如deptno、job); - 能写聚合函数(如
AVG(sal)、COUNT(*)); - 不能写"既不在 GROUP BY、又不是聚合"的裸列(如
ename)。
为什么?因为每个组会压缩成一行输出,而一个组里可能有多个不同的 ename——让 MySQL 输出哪个?它没法替你选。在 ONLY_FULL_GROUP_BY 模式下,这种写法直接报错;在没开这个模式的旧配置里,MySQL 会"随便挑一个"输出,行为不可靠、没有语义保证。
-- 这是不规范的(在 ONLY_FULL_GROUP_BY 下会报错):
-- ename 既不在 GROUP BY,也不是聚合函数,一个组里塞了多个名字,MySQL 无法决定输出谁
-- SELECT deptno, ename FROM emp GROUP BY deptno;所以记住大于等于学语法的一句话:分组查询里,SELECT 只写"分组依据的列 + 聚合函数",其余裸列一律别碰。
HAVING 过滤分组:跟 WHERE 到底差在哪
分组之后,我们还想"再筛掉一些组"。比如上面的需求进阶:显示平均工资低于 2000 的部门和它的平均工资。这里"平均工资"是分组后才算出来的,你没法用它套 WHERE(聚合不能放 WHERE)。这时就该 HAVING 上场——HAVING 专门用来过滤"分好之后"的组:
-- 先按部门分组求平均,再用 HAVING 把平均工资 < 2000 的部门留出来
SELECT deptno, AVG(sal) AS avg_sal
FROM emp
GROUP BY deptno
HAVING avg_sal < 2000;
-- 执行结果:
-- +--------+-----------+
-- | deptno | avg_sal |
-- +--------+-----------+
-- | 30 | 1766.67 |
-- +--------+-----------+三个部门的平均工资分别是 2750、2850、1766.67,只有部门 30 低于 2000,于是被保留。注意这里 HAVING avg_sal < 2000 里的别名 avg_sal 是可以用的——HAVING 发生在 SELECT 之后,别名对 HAVING 是"可见"的(又一次和 WHERE 形成对比)。
WHERE 与 HAVING 的区别,一张表讲透
这是极高频的考点,我把两者的分界线说得干干净净:
| 对比维度 | WHERE | HAVING |
|---|---|---|
| 过滤时机 | 分组之前,先过滤行 | 分组之后,过滤组 |
| 能否用聚合函数 | 不能(聚合尚未发生) | 能(组已分好) |
| 能否用 SELECT 别名 | 不能 | 能(MySQL 中) |
| 没有 GROUP BY 时 | 可以独立使用 | 一般配合 GROUP BY;MySQL 里无 GROUP BY 时 HAVING 作用近似 WHERE,但语义上它是给分组用的 |
一句话版本:WHERE 是"先筛行",进到分组阶段的数据是它筛剩下的;HAVING 是"筛组",是分组汇总之后做二次筛选。 所以"找及格的人"用 WHERE,"找平均分及格的班级"用 HAVING。
再补一个第 1 章就埋下的坑的官方答案——"WHERE 里不能用聚合函数" 就体现在:你永远无法用 WHERE 表达"比平均值高"这类需要先汇总的条件,这类条件统统交给"WHERE 先筛行 → GROUP BY 分组 → HAVING 筛组"的完整链路去完成。
帮你一根线穿起整条 SELECT:执行顺序
学到这里,有的同学可能已经有点"零件太多、转不过来"了。别慌,大纲里还有最后一块拼图——MySQL 处理一条查询语句的先后顺序。把这条顺序刻进脑子里,整篇文章的坑就全串起来了:
FROM -> WHERE -> GROUP BY -> HAVING -> SELECT -> DISTINCT -> ORDER BY -> LIMIT
逐段对应一下今天的内容,你会瞬间通透:
FROM:确定从哪张表取数;WHERE:逐行过滤(此时既没分组、也没别名、更没聚合结果);GROUP BY:把过滤后的行分组;HAVING:对分好的组做筛选;SELECT:挑列、计算表达式、起别名;DISTINCT:对 SELECT 结果去重;ORDER BY:对最终结果排序(此时别名已出生,所以能用);LIMIT:最后取一截分页。
现在回头看三个"为什么"就都能答上了:别名不能用在 WHERE(SELECT 还没走到);聚合不能用在 WHERE(分组汇总在更后面);HAVING 能用别名和聚合(它在 SELECT 和 GROUP BY 之后)。
思考题(附详解答案)
下面四道题把本文的关键点各考了一遍。建议先自己动手敲、别急着看答案,再对照下面的详解。
第 1 题:请说出下面这条语句的输出行数;如果把 COUNT(*) 换成 COUNT(qq),输出是什么?(假设 students 有 4 行,其中 qq 只有 1 个非 NULL)
SELECT COUNT(*), COUNT(qq) FROM students;详解答:一行输出,但两列各有一个值:第一列 COUNT(*) = 4(不管哪个字段,数的是总行数);第二列 COUNT(qq) = 1(只数 qq 非空的行,那 3 个 NULL 被跳过)。注意:聚合函数是"把一批行汇总成一行",所以哪怕 4 行输入,输出也只有 1 行、带两个汇总列。
第 2 题:我想找"数学成绩比全班数学平均分高的同学",下面三种写法哪个对、哪个错,为什么?
-- 写法一
SELECT name FROM exam_result WHERE math > AVG(math);
-- 写法二
SELECT name, AVG(math) FROM exam_result GROUP BY name HAVING math > AVG(math);
-- 写法三(伪代码思路)
SELECT name FROM exam_result WHERE math > (SELECT AVG(math) FROM exam_result);详解答:写法一错——聚合函数 AVG(math) 不能出现在 WHERE 里,WHERE 是逐行过滤,还轮不到汇总,直接语法报错。写法二想当然地错——它用 GROUP BY name 把同学按名字分组,HAVING math > AVG(math) 里的 math 既不在 GROUP BY 又不是聚合,在 ONLY_FULL_GROUP_BY 下非法;而且每条语句都自带一个 AVG(math),逻辑也不对。写法三才是对的方向——用子查询(一个查询嵌在另一个 WHERE 里,(SELECT AVG(math) FROM exam_result) 先算出全表平均分,再在外层逐行比较 math > 平均分)。子查询是后面要专门学的内容,这里你只要先看明白"想用汇总结果去逐行比较时,这条路是走通的"。
第 3 题:WHERE name LIKE '孙%' 和 WHERE name LIKE '孙_' 的语义差别,用一个有"孙悟空、孙权、孙"三个名字的表的匹配情况说明。
详解答:孙% 匹配"孙"开头、后面接任意多个(含 0 个)字符,所以"孙悟空""孙权"甚至只有"孙"单字都算命中;孙_ 匹配"孙"后面恰好一个字符,所以只命中恰好两个字的"孙权"(以及假设存在的"孙某"),"孙悟空"(三个字)和"孙"(少一个字)都不满足。一句话:% 是"任意长度通配",_ 是"恰好一位通配"。
第 4 题:下面这条按部门分组的查询,SELECT 里有个 ename,在 MySQL 默认(ONLY_FULL_GROUP_BY 开启)下会怎样?为什么?
SELECT deptno, ename, AVG(sal) FROM emp GROUP BY deptno;详解答:会直接报错。因为 GROUP BY deptno 把每部门压缩成一行,而 ename 既不在 GROUP BY 里、也不是聚合函数——一个部门有好几个员工姓名,MySQL 无法确定该输出哪一个,于是拒绝执行。正确的做法是:要么把 ename 去掉只保留"分组列 + 聚合"(SELECT deptno, AVG(sal) ... GROUP BY deptno),要么把 ename 也加进 GROUP BY(但那就变成按部门+姓名分组了,语义完全不同)。
到此,一条 SELECT 查询的整条流水线,我们算是从头到尾走了一遍:SELECT 负责挑列、起别名、算表达式;DISTINCT 负责去重;WHERE 负责先筛行,还要把 NULL 那两个难缠的运算符 IS NULL/IS NOT NULL 和模糊匹配的 %/_ 玩熟;ORDER BY 让结果有序、NULL 垫底;LIMIT 帮我们安全地取一截;聚合函数把整批行捏成一个数;GROUP BY 归堆,HAVING 筛组,最后用执行顺序这根线把 "WHERE 不能放聚合、HAVING 才能放"、"别名不能进 WHERE、却能进 ORDER BY 和 HAVING" 这些坑一次性串起来。
这些看似各管一段的技能,其实是环环相扣的:WHERE 筛出来的范围,决定聚合函数算的是谁的平均;GROUP BY 分组的粒度,决定 HAVING 筛的是哪一级的组;ORDER BY 的稳定排序,决定 LIMIT 分页是否靠谱。你越往后学(子查询、多表连接、索引优化),越会发现今天这些基础操作就是你搭建复杂查询的积木——地基打得越牢,后面的高楼越稳。
今日的练习建议格外简单粗暴也格外有效:把本文每个示例在自己的 MySQL 里亲手敲一遍,再把三道思考题改一改数字多试几种写法,亲眼看一眼每个报错长什么样。等到你闭着眼能说出"别名能用在 ORDER BY 和 HAVING、绝不能用进 WHERE"的时候,这一篇就算真正拿下了。下一篇文章,我们准备进入更复杂的检索世界——MySQL 的多表联合查询与连接,看看数据如何跨越好几张表被我们"拼"出来。准备好了吗?
还没有评论 — 第一条由你来留。