开篇介绍:
hello 大家,那么在我们使用MySQL语句进行查询的时候,我们老是会碰到一些奇奇怪怪的问题,那么其实大部分都是因为MySQL语句执行顺序的原因,所以,在本篇博客中,我们就来学习一下MySQL语句的执行顺序。
一、先立总纲:语法顺序 vs 执行顺序,天差地别
我们先明确两个核心概念:语法顺序是你写 SQL 的顺序(为了人类阅读方便),执行顺序是 MySQL 实际处理 SQL 的顺序(为了计算机高效运算)
1.1 你写 SQL 的 “语法顺序”(人类阅读顺序)
这是我们日常写 SQL 的顺序,符合人类的思维习惯 —— 先想 “要什么”,再想 “从哪来”“怎么筛选”:
SELECT 列1 [AS 别名1], 列2 [AS 别名2], 聚合函数(列3) [AS 别名3]
FROM 表1 [表别名1]
[INNER/LEFT/RIGHT JOIN 表2 [表别名2] ON 连接条件]
WHERE 行筛选条件(不能用聚合函数/别名)
GROUP BY 分组列1, 分组列2
HAVING 分组筛选条件(可以用聚合函数)
DISTINCT
ORDER BY 排序列 [ASC/DESC](可以用别名)
LIMIT 跳过行数 OFFSET 取行数;
1.2 MySQL 执行的 “底层顺序”(计算机运算顺序)
这是 MySQL 真正干活的顺序,遵循 “先找数据→再筛数据→再加工数据→最后展示数据” 的逻辑,和做菜的流程一模一样:
1. FROM → 2. ON → 3. JOIN → 4. WHERE → 5. GROUP BY → 6. HAVING → 7. SELECT → 8. DISTINCT → 9. ORDER BY → 10. LIMIT/OFFSET
1.3 核心类比升级:把 SQL 执行比作 “做一桌年夜饭”
为了让你理解,我们把这个执行顺序类比成 “做一桌包含番茄炒蛋、红烧肉、清蒸鱼的年夜饭”:
| 1. FROM,后面跟着表名,可以在这里给表名取别名,本质上join后面跟着的表名,也是算是在from后面的,from语句会一起收了,这一点大家注意 | 去菜市场采购食材(番茄、鸡蛋、五花肉、鱼、葱姜蒜) | 确定 “数据来源”—— 要操作哪些表 | 先搞清楚:做菜需要哪些原料?SQL 需要哪些表? |
| 2. ON,后面跟着筛选条件,不过是两个甚至多个表的 | 挑出 “匹配” 的食材(比如 “新鲜番茄” 配 “土鸡蛋”,“带皮五花肉” 配 “冰糖”) | 多表连接时,筛选 “匹配的行” | 不是所有食材都能放一起,得先挑出能搭配的 —— 就像学生表和成绩表,只有 “学生 ID 相同” 的行才能匹配 |
| 3. JOIN,后面也是跟着表名,一般用于多表查询,from后面跟一个,join后面跟一个,可以在这里给表取别名 | 把匹配的食材放进同一个大盆(番茄 + 鸡蛋放盆 A,五花肉 + 冰糖放盆 B) | 合并多表数据,生成第一张临时表 | 把挑好的搭配食材合并 —— 就像把学生表和成绩表的匹配行合并成 “学生 + 成绩” 的临时表 |
| 4. WHERE,后面跟着筛选条件 | 剔除坏食材(烂番茄扔掉、发臭的五花肉扔掉、破壳的鸡蛋扔掉) | 分组前筛选 “原始行数据”,生成第二张临时表 | 做菜前先挑出好食材 ——SQL 里就是先删掉不符合条件的行,减少后续工作量 |
| 5. GROUP BY,这个后面是跟着字段名,表示说要按照什么字段进行分组,相同值会被分到同一组 | 按菜品分盆(盆 A 做番茄炒蛋,盆 B 做红烧肉,盆 C 做清蒸鱼) | 按指定列分组,每组生成一行,生成第三张临时表 | 不同菜品要分开做 ——SQL 里就是相同分组列的行合并成一组,方便后续统计 |
| 6. HAVING,后面跟着筛选语句,分组之后呢,我们就可以对分的多个组进行筛选,只有满足我们的筛选条件的组,才有资格显示 | 剔除 “不够量” 的菜品(比如红烧肉的肉太少,直接放弃这道菜) | 分组后筛选 “统计结果”,生成第四张临时表 | 做完半成品后,筛掉不合格的 ——SQL 里就是删掉聚合结果不符合条件的分组 |
| 7. SELECT,后面跟着字段什么的,其实就是指要展示什么字段,只不过这些字段是经过了前面的那一些筛选之后,才轮到了select,而在select这里,我们就可以对字段取别名,而别名是在这里会被MySQL知道,所以聪明的你就知道了,为什么我们在select取别名时不能用到where哪里,就是因为执行的先后顺序,这个大家要理解哦,那么显然的,在select后面的语句,就可以使用别名来进行操作啦,而且聪明的你肯定也就意识到,我们要是在select语句执行之前,就给取别名的话,比如在from和join那里给表去别名,那么select这里,当然也可以直接用别名啦,因为别名已经在select执行之前被MySQL执行啦 | 烹饪 + 装盘(番茄炒蛋炒熟装白盘,红烧肉炖烂装红盘) | 选择要展示的列、计算表达式、定义别名,生成第五张临时表 | 把半成品做成成品,只装需要的部分 ——SQL 里就是只保留我们关心的列,计算总分、平均分 |
| 8. DISTINCT,这个是在select后面,字段前面,是将前面所筛选后的字段,也就是select之后的结果进行去重,这个大家之前也用过了,肯定不陌生 | 去掉重复的菜品(比如不小心做了两份番茄炒蛋,只留一份) | 对 SELECT 结果去重,生成第六张临时表 | 避免餐桌菜品重复 ——SQL 里就是删掉完全相同的行 |
| 9. ORDER BY,这个就是排序啦,desc是降序,asc是升序,将前面执行完所得到的结果进行排序 | 餐桌摆盘(红烧肉放中间,番茄炒蛋放左边,清蒸鱼放右边) | 对最终结果排序,生成第七张临时表 | 让菜品摆放更美观 ——SQL 里就是让数据按我们想要的顺序排列 |
| 10. LIMIT/OFFSET,这个就是最low的啦哈哈哈,毕竟他是负责要将多少内容给我们看,所以他肯定也就是最后执行的啦 | 上菜(只给客人上前两道菜,剩下的留着自己吃) | 分页返回结果,生成最终展示表 | 客人吃不完所有菜,只上一部分 ——SQL 里就是避免一次性返回太多数据,卡死数据库 |
这个类比的核心是:MySQL 执行 SQL 的顺序,是 “从源头到结果” 的逐步加工过程,每一步都生成一张临时表,供下一步使用—— 就像做菜的每一步都生成一个半成品,供下一步加工。
二、逐步骤拆解:
接下来,我们对每个执行步骤进行拆解
步骤 1:FROM —— 确定数据来源,SQL 执行的 “源头”
底层原理
这是 SQL 执行的第一步,没有例外——MySQL 会先根据FROM后的表名,找到对应的表文件(存储在磁盘上),然后把表的元数据(表结构、列类型、索引信息)加载到内存,再根据需要,逐步把表的数据行加载到内存的临时区域(比如join_buffer)。
这里的关键是:MySQL 是 “按需加载” 数据,不是一次性把整张表加载到内存 —— 比如表有 100 万行,WHERE条件只需要 1 万行,MySQL 会只加载这 1 万行,节省内存。
实战示例
单表查询:
SELECT name, math FROM exam_result;
执行FROM exam_result时,MySQL 会先找到exam_result表的元数据,然后准备加载该表的数据行(后续步骤会根据需要筛选)。
多表查询:
SELECT s.name, e.math FROM students s JOIN exam_result e ON s.id = e.student_id;
执行FROM students s时,先加载students表的元数据和数据行;然后执行JOIN exam_result e时,再加载exam_result表的元数据和数据行 ——多表的加载顺序,会影响后续 JOIN 的效率。
常见误区
误区 1:认为FROM是 “最后执行” 的,因为写在 SQL 的第二行
正解:FROM是第一步执行的,SQL 的语法顺序是为了人类阅读,和执行顺序无关。
误区 2:FROM后可以写任意表名,不管表是否存在
正解:如果FROM后的表不存在,MySQL 会直接报错Table 'xxx' doesn't exist,后续步骤都不会执行。
性能优化技巧
SELECT name, math FROM (SELECT * FROM exam_result WHERE math > 60) AS t;
子查询会先筛选出math>60的行,生成临时表t,再从t里查询 —— 比直接查大表效率高。
边缘情况:临时表和视图的处理
- 临时表:FROM后可以跟临时表(比如CREATE TEMPORARY TABLE t …),临时表只存在于当前会话,关闭会话后自动删除,执行效率和普通表一样。
- 视图:FROM后可以跟视图(比如CREATE VIEW v AS SELECT …),视图是 “虚拟表”,MySQL 执行时会先把视图转换成对应的 SELECT 语句,再执行 —— 视图本身不存储数据,性能和直接写 SELECT 一样。
步骤 2:ON —— 多表连接的 “匹配筛选器”
底层原理
这是多表 JOIN 专属的步骤,只在有JOIN时执行。它的作用是:在两张表的数据行合并之前,先筛选出 “匹配” 的行—— 就像做菜前,先挑出能搭配的食材,避免把所有食材都混在一起。
ON的匹配逻辑是:遍历驱动表的每一行,然后在被驱动表里找满足ON条件的行 ——只有满足条件的行,才会被保留到下一步。
这里的关键是:ON是在JOIN合并数据之前执行的,所以能减少后续 JOIN 的数据量—— 这是提升多表查询效率的关键。
on这个小baby,那可是我们后面讲到复合查询的常客,靠他解决笛卡尔积
实战示例:不同 JOIN 类型下的 ON 表现
我们用students表(左表)和exam_result表(右表)来举例,students表有一行 “学生 A” 但exam_result表没有他的成绩(即右表无匹配行)。
INNER JOIN(内连接):
SELECT s.name, e.math FROM students s INNER JOIN exam_result e ON s.id = e.student_id;
ON会筛选出s.id = e.student_id的行,无匹配的行(比如学生 A)会被直接丢弃,不会进入下一步。
LEFT JOIN(左连接):
SELECT s.name, e.math FROM students s LEFT JOIN exam_result e ON s.id = e.student_id;
ON会筛选出匹配的行,左表的无匹配行(学生 A)不会被丢弃,而是保留下来,右表的列填 NULL—— 这是左连接和内连接的核心区别。
RIGHT JOIN(右连接):
SELECT s.name, e.math FROM students s RIGHT JOIN exam_result e ON s.id = e.student_id;
和左连接相反,右表的无匹配行会被保留,左表的列填 NULL。
那么关于左右内连接,我们后面会有一篇博客讲到。
常见误区
误区 1:ON和WHERE的作用一样,可以随便替换
正解:内连接中,ON 和 WHERE 的效果一样;但外连接中,两者天差地别!比如左连接中,把ON s.id = e.student_id改成WHERE s.id = e.student_id,会导致左表的无匹配行被丢弃 —— 因为WHERE是在 JOIN 之后执行的,会筛选掉 NULL 值的行。
误区 2:ON里只能写连接条件,不能写其他筛选条件
正解:ON里可以写任何筛选条件,优先把筛选条件放在ON里,能提升效率 —— 比如:
SELECT s.name, e.math FROM students s LEFT JOIN exam_result e ON s.id = e.student_id AND e.math > 60;
这个语句会先筛选出math>60的成绩行,再和学生表连接 —— 比放在WHERE里效率高,因为减少了 JOIN 的数据量。
性能优化技巧
边缘情况:NULL 值在 ON 中的处理
ON条件里,NULL 和任何值的比较结果都是 NULL,不是 FALSE—— 比如ON e.math = NULL,永远不会匹配到任何行。如果要判断 NULL 值,必须用<=>(NULL 安全的等于),比如ON e.math <=> NULL—— 这样才能匹配到math为 NULL 的行。
步骤 3:JOIN —— 合并多表数据,生成第一张临时表
底层原理
这一步的作用是:把ON筛选后的多表数据行,合并成一张临时表—— 就像把挑好的搭配食材放进同一个盆里,准备烹饪。
JOIN的底层实现有三种算法,MySQL 会根据表的大小和索引情况自动选择:
不管用哪种算法,JOIN的最终结果都是生成一张包含所有匹配行的临时表,这张临时表会传到下一步WHERE使用。
实战示例:JOIN 生成的临时表长什么样
我们用一个简单的例子来说明:
- students表有两行:(1, '孙悟空'), (2, '唐三藏')
- exam_result表有两行:(1, 98), (3, 60)
- 执行SELECT s.id, s.name, e.math FROM students s LEFT JOIN exam_result e ON s.id = e.exam_id;左连接,会把左边的表都展示出来,如果没有右边相对应的,那就显示null
ON筛选后,匹配的行是s.id=1,左表的s.id=2无匹配行;JOIN合并后生成的临时表是:
| 1 | 孙悟空 | 98 |
| 2 | 唐三藏 | NULL |
这张临时表就是下一步WHERE的输入数据。
常见误区
误区 1:多表 JOIN 的顺序不影响性能
正解:JOIN 顺序影响巨大!MySQL 默认会选择 “小表驱动大表” 的顺序,但如果表很大,最好手动调整顺序 —— 比如FROM 小表A JOIN 大表B JOIN 超大表C,比反过来的顺序效率高 10 倍。
误区 2:JOIN 越多,查询结果越全
正解:JOIN 越多,数据量越大,效率越低,还容易产生笛卡尔积—— 比如两张各有 1000 行的表,不加ON条件直接 JOIN,会生成 1000*1000=100 万行的临时表,直接卡死数据库。
性能优化技巧
边缘情况:笛卡尔积的产生和避免
当ON条件缺失或无效时,JOIN会生成笛卡尔积—— 即左表的每一行和右表的每一行都组合一次,数据量爆炸式增长。比如:
SELECT * FROM students s JOIN exam_result e; — 没有ON条件
如果students有 100 行,exam_result有 100 行,会生成 100*100=10000 行的临时表 ——生产环境中,绝对要避免笛卡尔积!
步骤 4:WHERE —— 分组前的 “数据过滤器”
底层原理
这一步的作用是:对JOIN生成的临时表,进行 “第一次筛选”,只保留符合条件的行—— 就像做菜前,把盆里的坏食材挑出来扔掉。
WHERE的执行逻辑是:逐行判断—— 遍历临时表的每一行,满足WHERE条件的行保留,不满足的行直接删除,不进入下一步。
这里的关键是:WHERE是在分组前执行的,所以它筛选的是 “原始行数据”,不是分组后的统计结果—— 这就是为什么WHERE里不能用聚合函数。
实战示例:WHERE的筛选效果
我们用步骤 3 生成的临时表来举例:
| 1 | 孙悟空 | 98 |
| 2 | 唐三藏 | NULL |
执行WHERE e.math > 60:
- 第一行e.math=98>60,保留;
- 第二行e.math=NULL,NULL>60的结果是 NULL(不是 FALSE),所以删除;
最终生成的临时表是:
| 1 | 孙悟空 | 98 |
常见误区
误区 1:WHERE里可以用聚合函数
正解:绝对不能用!因为WHERE在分组前执行,聚合函数(比如SUM、AVG)需要分组后才能计算 —— 比如:
— 错误!会报 Invalid use of group function
SELECT class, AVG(math) FROM exam_result WHERE AVG(math) > 80 GROUP BY class;
正确的做法是把聚合函数移到HAVING里:
SELECT class, AVG(math) FROM exam_result GROUP BY class HAVING AVG(math) > 80;
误区 2:WHERE里可以用SELECT的别名
正解:绝对不能用!因为WHERE在SELECT前执行,SELECT的别名还没定义 —— 比如:
— 错误!会报 Unknown column '总分' in 'where clause'
SELECT name, math + chinese + english AS 总分 FROM exam_result WHERE 总分 > 200;
正确的做法是用原始表达式:
SELECT name, math + chinese + english AS 总分 FROM exam_result WHERE math + chinese + english > 200;
误区 3:WHERE里判断 NULL 用=
正解:=对 NULL 无效,必须用IS NULL或IS NOT NULL—— 比如:
— 错误!永远返回空结果
SELECT name FROM students WHERE qq = NULL;
— 正确
SELECT name FROM students WHERE qq IS NULL;
如果要同时判断普通值和 NULL,可以用<=>(NULL 安全的等于):
SELECT name FROM students WHERE qq <=> NULL; — 等价于 qq IS NULL
性能优化技巧
边缘情况:WHERE在不同 SQL 类型中的表现
- 单表查询:没有JOIN和ON,FROM之后直接执行WHERE—— 逻辑更简单,效率更高。
- 子查询中的WHERE:子查询里的WHERE会先执行,生成小的临时表,再和主查询的表连接 —— 比如FROM (SELECT * FROM t WHERE status=1) AS sub,子查询的WHERE先筛选数据,这还是因为括号的优先级最高哦,而在括号里,又是from第一,where第二,所以你一下子就理解了吧
步骤 5:GROUP BY —— 数据的 “分类器”
底层原理
这一步的作用是:把WHERE筛选后的临时表,按指定列 “分组”,相同分组列的行合并成一行—— 就像把盆里的食材按菜品分类,每类菜品单独处理。
GROUP BY的底层实现有两种方式:
MySQL 会根据数据量自动选择分组方式 —— 数据量小时用排序分组,数据量大时用哈希分组。
分组后,临时表的结构会发生本质变化:每组只有一行数据,后续的HAVING、SELECT只能对 “组” 进行操作,不能对单个行操作。
实战示例:分组后的临时表长什么样
我们用exam_result表的部分数据举例:
| 孙悟空 | 一班 | 98 |
| 唐三藏 | 一班 | 80 |
| 猪悟能 | 二班 | 90 |
执行GROUP BY class:
- 一班的两行合并成一组,二班的一行单独成一组;
- 分组后生成的临时表是(此时还没计算聚合函数):| class ||——-|| 一班 || 二班 |
这张临时表会传到下一步HAVING,然后在SELECT里计算聚合函数(比如AVG(math))。
常见误区
误区 1:SELECT里可以用非分组列
正解:严格模式下,绝对不能用!分组后每组只有一行,非分组列的值是不确定的 —— 比如:
— 严格模式下报错:Expression #1 of SELECT list is not in GROUP BY clause
SELECT name, class, AVG(math) FROM exam_result GROUP BY class;
为什么会报错?因为一班有 “孙悟空” 和 “唐三藏” 两个名字,MySQL 不知道该选哪个 —— 宽松模式下会随机选一个,这会导致数据错误!正确的做法是:SELECT里只能出现分组列和聚合函数:
SELECT class, AVG(math) FROM exam_result GROUP BY class;
当然,要是我们不group by的话,那么select还是可以随心所欲滴。
误区 2:多列分组的顺序不影响结果
正解:影响分组的粒度!多列分组的顺序是 “先按第一列分,再按第二列分”—— 比如GROUP BY class, gender:
- 先按class分成一班、二班;
- 再在每个班里,按gender分成男生、女生;最终的分组粒度是 “班级 + 性别”,比单分class更细。
误区 3:GROUP BY会自动排序
正解:MySQL 5.7 及以前会自动排序,MySQL 8.0 及以后不会!比如GROUP BY class,MySQL 5.7 会按class升序排列,MySQL 8.0 不会 —— 如果需要排序,必须显式加ORDER BY。
性能优化技巧
边缘情况:隐式分组和空值分组
- 隐式分组:如果SELECT里有聚合函数,但没有GROUP BY,MySQL 会把整个临时表当作 “一组”—— 比如SELECT AVG(math) FROM exam_result,就是计算所有学生的平均分。看到这个,聪明的你就知道了,为什么明明说聚合函数只能给组使用,但是我们之前使用的时候,明明没有group by,却还是能使用聚合函数了,因为MySQL会直接把一整个表当成一个组哦。
- 空值分组:分组列的 NULL 值会被当作一组—— 比如GROUP BY qq,所有qq为 NULL 的学生都会被分到同一组。
步骤 6:HAVING —— 分组后的 “结果过滤器”
底层原理
这一步的作用是:对GROUP BY生成的分组临时表,进行 “第二次筛选”,只保留符合条件的分组—— 就像做完半成品后,筛掉不够量的菜品。
HAVING和WHERE的核心区别是:HAVING筛选的是 “分组后的统计结果”,WHERE筛选的是 “分组前的原始行数据”—— 所以HAVING里可以用聚合函数,WHERE不能。还是因为聚合函数只能对组使用哦,那么很显然,where肯定是比having要先执行的,where筛选之后的结果,才被拿来分组,而having则是对分组之后的组,进行筛选,这个还是很好理解的各位。
HAVING的执行逻辑是:遍历每个分组,计算聚合函数的值,满足HAVING条件的分组保留,不满足的删除。
实战示例:HAVING的筛选效果
我们用步骤 5 的分组临时表举例,执行SELECT class, AVG(math) AS avg_math FROM exam_result WHERE math>60 GROUP BY class HAVING avg_math>80;:
常见误区
误区 1:HAVING和WHERE可以随便替换
正解:绝对不能!两者的执行时机和筛选对象完全不同:
| 执行时机 | 分组前 | 分组后 |
| 筛选对象 | 原始行数据 | 分组统计结果 |
| 能否用聚合函数 | ❌ 不能 | ✅ 可以 |
| 效率 | 高(筛选后减少分组数据量) | 低(分组后再筛选) |
| 记住:能放在WHERE里的筛选条件,绝不放在HAVING里! |
误区 2:HAVING里可以用SELECT的别名
正解:MySQL 5.7 及以后可以,MySQL 5.6 及以前不可以!比如HAVING avg_math>80,avg_math是SELECT的别名 —— 为了兼容性,建议用聚合函数的原始写法,比如HAVING AVG(math)>80。
误区 3:HAVING可以省略GROUP BY
正解:可以!如果省略GROUP BY,MySQL 会把整个表当作一组,HAVING筛选的是整个表的聚合结果 —— 比如SELECT AVG(math) FROM exam_result HAVING AVG(math)>80,就是判断所有学生的平均分是否 > 80。oioi,这一点还是值得大家注意一下
性能优化技巧
边缘情况:HAVING在子查询中的应用
HAVING可以用在子查询里,筛选分组后的结果 —— 比如:
SELECT * FROM (SELECT class, AVG(math) AS avg_math FROM exam_result GROUP BY class) AS t WHERE t.avg_math>80;
这个语句里,子查询的GROUP BY生成分组结果,主查询的WHERE筛选平均分 > 80 的班级 —— 效果和HAVING一样,但兼容性更好(所有 MySQL 版本都支持)。
步骤 7:SELECT —— 数据的 “烹饪装盘器”
底层原理
这一步的作用是:从HAVING筛选后的分组临时表中,选择要展示的列、计算表达式、定义别名—— 就像把半成品做成成品,只装需要的部分。
SELECT的执行逻辑是:
这里的关键是:SELECT是在筛选和分组之后执行的,所以它只是 “展示数据”,不会改变数据的内容—— 就像装盘不会改变菜的味道,只是改变菜的呈现方式。
实战示例:SELECT的列裁剪和表达式计算
我们用步骤 6 的分组临时表举例,执行SELECT class AS 班级, AVG(math) AS 平均数学成绩 FROM exam_result WHERE math>60 GROUP BY class HAVING AVG(math)>80;:
常见误区
误区 1:SELECT *效率很高
正解:效率极低!SELECT *会返回所有列,包括不需要的列 —— 增加内存消耗和网络传输量。比如表有 20 列,你只需要 2 列,SELECT *会多传输 18 列的数据,速度慢很多。正确的做法是:只选需要的列—— 比如SELECT class, AVG(math) FROM …。
误区 2:别名可以在WHERE/GROUP BY/HAVING里用
正解:绝对不能!别名是SELECT定义的,SELECT在WHERE、GROUP BY、HAVING之后执行 —— 所以这些步骤无法识别别名。只有ORDER BY和LIMIT能识别别名。
误区 3:SELECT里的表达式会改变原始数据正解:不会!SELECT里的表达式只是 “计算展示结果”,不会修改数据库里的原始数据 —— 比如SELECT math+10 FROM exam_result,只是展示数学成绩加 10 分的结果,不会把数据库里的math改成加 10 分。
性能优化技巧
边缘情况:SELECT里的函数和常量
- 函数使用:SELECT里可以用各种函数,比如SELECT UPPER(name) FROM students(把名字转成大写),SELECT NOW() FROM dual(查询当前时间)—— 函数的计算结果会作为列值展示。
- 常量列:SELECT里可以直接写常量,比如SELECT name, 10 FROM students—— 会给每一行增加一个值为 10 的列,这在测试时很有用。
步骤 8:DISTINCT —— 数据的 “去重器”
底层原理
这一步的作用是:对SELECT生成的临时表,去掉完全重复的行—— 就像去掉餐桌上重复的菜品。
DISTINCT的底层实现有两种方式:
DISTINCT的关键是:它作用于SELECT的所有列,不是单个列—— 比如SELECT DISTINCT class, gender FROM students,是对 “班级 + 性别” 的组合去重,不是只对class去重。
实战示例:DISTINCT的去重效果
我们用students表的部分数据举例:
| 孙悟空 | 一班 | 男 |
| 唐三藏 | 一班 | 男 |
| 猪悟能 | 二班 | 男 |
执行SELECT DISTINCT class, gender FROM students:
- “一班 + 男” 出现了两次,去重后只保留一次;
- “二班 + 男” 出现一次,保留;最终的临时表是:| class | gender ||——-|——–|| 一班 | 男 || 二班 | 男 |
常见误区
误区 1:DISTINCT只作用于紧跟它的列
正解:作用于所有列!比如SELECT DISTINCT class, name FROM …,是对class和name的组合去重,不是只对class去重 —— 如果class相同但name不同,不会被去重这一点之前我也有讲过哦
误区 2:DISTINCT和GROUP BY可以随便替换正解:单列去重可以替换,多列去重不一定!比如SELECT DISTINCT class FROM …等价于SELECT class FROM … GROUP BY class;但SELECT DISTINCT class, name FROM …等价于SELECT class, name FROM … GROUP BY class, name——GROUP BY的效率通常比DISTINCT高,因为GROUP BY可以用索引。
误区 3:DISTINCT会自动排序
正解:和GROUP BY一样,MySQL 5.7 及以前会排序,MySQL 8.0 及以后不会—— 如果需要排序,必须加ORDER BY。
性能优化技巧
边缘情况:DISTINCT和聚合函数的结合
DISTINCT可以和聚合函数结合,计算去重后的统计结果 —— 比如SELECT COUNT(DISTINCT class) FROM students,是统计不同班级的数量;SELECT SUM(DISTINCT math) FROM exam_result,是计算不同数学成绩的总和。
步骤 9:ORDER BY —— 数据的 “排序器”
底层原理
这一步的作用是:对DISTINCT生成的临时表,按指定列排序—— 就像给餐桌上的菜品摆盘,让它更美观。
ORDER BY的底层实现和GROUP BY类似,有排序排序和哈希排序两种方式,但ORDER BY主要用排序排序—— 因为需要保证数据的有序性。
ORDER BY的关键是:它是第一个可以使用SELECT别名的步骤—— 因为ORDER BY在SELECT之后执行,别名已经定义。
实战示例:ORDER BY的排序效果
我们用步骤 8 的临时表举例,执行SELECT class AS 班级, AVG(math) AS 平均数学成绩 FROM exam_result WHERE math>60 GROUP BY class HAVING AVG(math)>80 ORDER BY 平均数学成绩 DESC;:
- 按 “平均数学成绩” 降序排序,平均分高的班级排在前面;
- 如果两个班级的平均分相同,会按class升序排列(默认)。
常见误区(新手必踩)
误区 1:ORDER BY里不能用SELECT的别名
正解:可以用!这是ORDER BY的专属特权 —— 比如ORDER BY 平均数学成绩 DESC比ORDER BY AVG(math) DESC更简洁、更易读。
误区 2:ORDER BY的排序是稳定的
正解:默认不稳定!如果排序列的值相同,MySQL 不保证这些行的相对顺序 —— 比如两个班级的平均分都是 89,第一次查询可能一班在前,第二次可能二班在前。如果需要稳定排序,可以加一个唯一列(比如id)——ORDER BY 平均数学成绩 DESC, id ASC。
误区 3:ORDER BY对 NULL 值的处理和普通值一样
正解:不一样!MySQL 中,NULL 被视为最小值—— 升序排序时,NULL 排在最前面;降序排序时,NULL 排在最后面。比如ORDER BY qq ASC,qq为 NULL 的学生排在最前面;ORDER BY qq DESC,qq为 NULL 的学生排在最后面。
性能优化技巧
边缘情况:ORDER BY里的函数和表达式
ORDER BY里可以用函数和表达式 —— 比如ORDER BY UPPER(class) ASC(按班级名大写排序),ORDER BY math+chinese+english DESC(按总分降序排序)—— 但函数和表达式会导致索引失效,尽量用列名或别名排序。
步骤 10:LIMIT/OFFSET —— 数据的 “分页器”
底层原理
这是 SQL 执行的最后一步,作用是:对ORDER BY生成的排序临时表,分页返回结果—— 就像只给客人上前两道菜,剩下的留着自己吃。
LIMIT/OFFSET的语法是:LIMIT 取行数 OFFSET 跳过行数—— 比如LIMIT 3 OFFSET 0是取前 3 行,LIMIT 3 OFFSET 3是取第 4-6 行。
LIMIT的关键是:它必须和ORDER BY一起使用—— 否则数据的顺序不确定,分页结果会随机变化。
实战示例:LIMIT的分页效果
我们用步骤 9 的排序临时表举例,表中有 5 个班级,按平均分降序排列:
| 一班 | 89 |
| 三班 | 85 |
| 二班 | 82 |
| 五班 | 78 |
| 四班 | 75 |
执行LIMIT 2 OFFSET 0:取前 2 行,返回一班、三班;执行LIMIT 2 OFFSET 2:取第 3-4 行,返回二班、五班;执行LIMIT 2 OFFSET 4:取第 5 行,返回四班(不足 2 行,只返回 1 行)。
常见误区(新手必踩)
误区 1:LIMIT可以不用ORDER BY
正解:绝对不能!没有ORDER BY,数据的顺序是不确定的 —— 比如第一次LIMIT 2返回一班、三班,第二次可能返回二班、五班,分页结果完全混乱。
误区 2:OFFSET越大,效率越高
正解:OFFSET越大,效率越低!比如LIMIT 2 OFFSET 100000,MySQL 需要先遍历 10 万行数据,再取 2 行 —— 这会导致查询非常慢。解决方案:游标分页—— 用唯一列(比如id)代替OFFSET,比如WHERE id > 100000 LIMIT 2——MySQL 可以用索引快速定位到id>100000的行,效率大幅提升。
误区 3:LIMIT会修改原始数据
正解:不会!LIMIT只是 “限制返回的行数”,不会修改数据库里的原始数据 —— 比如LIMIT 2只是返回前 2 行,不会删除后面的行。
性能优化技巧
sql
— 第1页:id<100 LIMIT 2
SELECT * FROM exam_result WHERE id < 100 ORDER BY id DESC LIMIT 2;
— 第2页:id<80 LIMIT 2(假设第1页的最后一个id是80)
SELECT * FROM exam_result WHERE id < 80 ORDER BY id DESC LIMIT 2;
边缘情况:LIMIT在无ORDER BY时的表现
如果没有ORDER BY,LIMIT会返回 “MySQL 认为的前 N 行”—— 这通常是数据在磁盘上的存储顺序,完全随机,没有任何意义 ——生产环境中,绝对要避免这种用法!
三、实战升华:复杂 SQL 的执行顺序拆解
3.1 复杂 SQL 案例:多表 JOIN + 分组 + 排序 + 分页
我们用一个综合案例,完整拆解执行顺序,让你把前面的知识点融会贯通:
SELECT
s.class AS 班级,
COUNT(DISTINCT s.id) AS 学生数,
AVG(e.math) AS 平均数学成绩
FROM
students s
LEFT JOIN
exam_result e ON s.id = e.student_id AND e.math > 60
WHERE
s.gender = '男'
GROUP BY
s.class
HAVING
COUNT(DISTINCT s.id) > 5
ORDER BY
平均数学成绩 DESC
LIMIT
3 OFFSET 0;
3.2 执行顺序完整拆解(一步一步来)
| 1. FROM | 加载students s表的元数据和数据行 | 包含所有学生的原始数据 |
| 2. ON | 筛选s.id = e.student_id且e.math>60的匹配行;左表无匹配行的e.math填 NULL | 包含男生 + 匹配的成绩行,以及男生 + NULL 成绩行 |
| 3. JOIN | 合并学生表和成绩表的匹配行,生成临时表 T1 | T1 的列:s.id, s.name, s.class, s.gender, e.math |
| 4. WHERE | 筛选s.gender='男'的行,生成临时表 T2 | T2 只包含男生的行,排除女生 |
| 5. GROUP BY | 按s.class分组,生成临时表 T3 | T3 的列:s.class;每组对应一个班级 |
| 6. HAVING | 筛选COUNT(DISTINCT s.id)>5的分组,生成临时表 T4 | T4 只包含学生数 > 5 的班级 |
| 7. SELECT | 选择s.class(别名班级)、COUNT(DISTINCT s.id)(别名学生数)、AVG(e.math)(别名平均数学成绩),生成临时表 T5 | T5 的列:班级,学生数,平均数学成绩 |
| 8. DISTINCT | T5 无重复行,直接生成 T6=T5 | T6 和 T5 一样 |
| 9. ORDER BY | 按 “平均数学成绩” 降序排序,生成临时表 T7 | T7 的班级按平均分从高到低排列 |
| 10. LIMIT | 跳过 0 行,取前 3 行,生成最终结果表 | 最终结果:前 3 个平均分最高的班级 |
3.3 用 EXPLAIN 验证执行顺序
EXPLAIN是 MySQL 的 “执行计划查看工具”,可以帮你验证 SQL 的执行顺序 —— 执行EXPLAIN + 你的SQL,会返回一张执行计划表,关键字段的含义如下:
- id:执行顺序的标识 ——id 相同的步骤,按从上到下的顺序执行;id 不同的步骤,id 越大,执行顺序越靠前。
- select_type:查询的类型 —— 比如SIMPLE(简单查询)、DERIVED(子查询)、JOIN(连接查询)。
- table:当前步骤操作的表 —— 对应FROM和JOIN的表。
- type:访问类型 ——ALL(全表扫描)、index(索引扫描)、range(范围扫描)、ref(索引匹配)、const(常量匹配)——type越好,效率越高。
- Extra:额外信息 —— 比如Using index(使用索引)、Using where(使用 WHERE 筛选)、Using temporary(使用临时表)、Using filesort(使用文件排序)—— 这些信息可以帮你定位性能瓶颈。
比如执行EXPLAIN上面的复杂 SQL,会看到:
- table列先出现s(学生表),再出现e(成绩表)—— 对应FROM和JOIN的顺序;
- Extra列有Using where—— 对应WHERE筛选;
- Extra列有Using temporary—— 对应GROUP BY生成临时表;
- Extra列有Using filesort—— 对应ORDER BY排序。
四、终极避坑指南
| 1. WHERE 用聚合函数 | SELECT class, AVG(math) FROM t WHERE AVG(math)>80 GROUP BY class; | WHERE 在分组前执行,聚合函数未计算 | 把聚合函数移到 HAVING:HAVING AVG(math)>80 |
| 2. WHERE 用 SELECT 别名 | SELECT math+chinese 总分 FROM t WHERE 总分>200; | WHERE 在 SELECT 前执行,别名未定义 | 用原始表达式:WHERE math+chinese>200 |
| 3. SELECT 用非分组列 | SELECT name, class FROM t GROUP BY class; | 严格模式下,非分组列的值不确定 | 只选分组列 + 聚合函数:SELECT class, AVG(math) FROM t GROUP BY class; |
| 4. HAVING 替代 WHERE | SELECT class FROM t GROUP BY class HAVING math>60; | HAVING 在分组后筛选,效率低 | 用 WHERE 先筛选:WHERE math>60 GROUP BY class; |
| 5. ORDER BY 不用别名 | SELECT AVG(math) avg_math FROM t GROUP BY class ORDER BY AVG(math) DESC; | 语法冗余,不易读 | 用别名排序:ORDER BY avg_math DESC; |
| 6. LIMIT 不用 ORDER BY | SELECT * FROM t LIMIT 10; | 数据顺序不确定,分页结果随机 | 加 ORDER BY:ORDER BY id LIMIT 10; |
| 7. OFFSET 分页大数据量 | SELECT * FROM t ORDER BY id LIMIT 2 OFFSET 100000; | OFFSET 越大,效率越低 | 用游标分页:WHERE id>100000 LIMIT 2; |
| 8. DISTINCT 多列去重 | SELECT DISTINCT class, name, gender FROM t; | 多列去重效率低,临时表大 | 用 GROUP BY 替代:SELECT class, name, gender FROM t GROUP BY class, name, gender; |
| 9. ON 和 WHERE 混淆(外连接) | SELECT s.name, e.math FROM s LEFT JOIN e ON s.id=e.id WHERE e.math>60; | WHERE 会删除左表无匹配的行 | 把条件移到 ON:ON s.id=e.id AND e.math>60; |
| 10. JOIN 无 ON 条件 | SELECT * FROM s JOIN e; | 生成笛卡尔积,数据量爆炸 | 加 ON 条件:JOIN e ON s.id=e.student_id; |
五、总结
5.1 核心逻辑(牢记这三句话)
最后,大家复习一下:from、on、join、where、group by、having、select、distinct、order by、limit。
结语:从 “会写 SQL” 到 “写好 SQL”,执行顺序是你最坚实的底气
当你看到这里,我想你大概率和曾经的我一样,在学习 MySQL 的路上有过这样的时刻:明明照着教程写的 SQL 语句,却报出 “Unknown column” 的错误;明明筛选条件写对了,结果却少了一大半数据;明明想优化慢查询,却对着几十行的 SQL 无从下手…… 而这一切的核心,答案都藏在我们今天聊透的 “执行顺序” 里。
我始终觉得,学习 SQL 最忌讳的就是 “只记语法,不问底层”。就像做菜时只记 “放油、放菜、放盐” 的步骤,却不知道 “为什么先放油”“为什么盐要后放”,一旦换个食材、改个口味,就手忙脚乱。我们把 SQL 执行顺序比作 “做年夜饭”,不只是为了让抽象的逻辑变具象,更是想让你明白:MySQL 执行 SQL 的过程,和我们处理一件事的底层逻辑是相通的 —— 先明确来源(FROM),再筛选匹配(ON/JOIN/WHERE),再分类处理(GROUP BY/HAVING),最后包装呈现(SELECT/DISTINCT/ORDER BY/LIMIT)。每一步都不是孤立的,而是数据从 “原始原料” 到 “成品展示” 的流转,每一步生成的临时表,都是下一步的基础。
你可能会觉得,“记熟 10 步执行顺序就够了”,但我想告诉你,这只是起点。真正的掌握,是你看到任何一条复杂 SQL 时,都能下意识地拆解它的执行流程:先看 FROM 和 JOIN 确定数据来源,再看 ON 和 WHERE 判断数据筛选的时机,接着看 GROUP BY 和 HAVING 明确分组逻辑,最后看 SELECT 到 LIMIT 知道数据如何展示。就像我们拆解那个 “多表 JOIN + 分组 + 排序 + 分页” 的案例时,每一步临时表的变化,都是数据 “被加工” 的痕迹,理解了这些痕迹,你就不会再犯 “WHERE 里用聚合函数”“LIMIT 不加 ORDER BY” 这样的低级错误 —— 因为你知道,不是语法不允许,而是执行时机不支持。
我见过太多新手朋友,把精力花在背 “聚合函数大全”“JOIN 类型对比” 上,却忽略了 “执行顺序” 这个最核心的骨架。就像搭积木,没有骨架的支撑,再多的零件也拼不出稳定的结构。你可以忘记某个聚合函数的写法,忘记 DISTINCT 和 GROUP BY 的细微差异,但只要记住 “FROM→ON→JOIN→WHERE→GROUP BY→HAVING→SELECT→DISTINCT→ORDER BY→LIMIT” 这个核心流转逻辑,就能通过查手册、拆步骤,解决 80% 的 SQL 问题。而那些我们反复强调的 “避坑点”—— 比如外连接中 ON 和 WHERE 的区别、OFFSET 分页的效率问题、SELECT 别名的使用范围,本质上都是对执行顺序理解不到位的体现。
当然,我也知道,理解执行顺序的过程不会一蹴而就。你可能会在第一次用 EXPLAIN 查看执行计划时,对着 “Using temporary”“Using filesort” 这些关键词一头雾水;可能会在拆解多表 JOIN 的执行步骤时,分不清临时表 T1 和 T2 的差异;可能会在优化慢查询时,不知道该把筛选条件放在 ON 还是 WHERE 里…… 但请你相信,每一次 “卡壳” 都是进步的契机。就像做菜,第一次炒番茄炒蛋会糊锅,第一次炖红烧肉会咸,但多做几次,理解了 “火候” 和 “调味时机” 的关系,就能熟能生巧。SQL 的学习也是如此,多写、多拆、多验证,把我们今天讲的执行顺序融入到每一次写 SQL 的过程中,从单表查询到多表关联,从简单分组到复杂分页,慢慢的,你会发现曾经让你头疼的报错、让你困惑的结果,都能从执行顺序里找到答案。
更重要的是,掌握执行顺序,不只是让你 “写出能跑的 SQL”,更是让你 “写出高效的 SQL”。在生产环境中,我们面对的不是只有 7 行数据的 exam_result 表,而是几十万、几百万甚至几千万行的业务表。此时,“早筛选,晚排序” 的优化原则就显得尤为重要 —— 把能在 WHERE 里筛选的条件,绝不放到 HAVING 里;把能在 ON 里匹配的条件,绝不留给 WHERE;给分组列、排序列建索引,用游标分页替代 OFFSET 分页…… 这些优化技巧,本质上都是在顺应 MySQL 的执行顺序,让数据在流转的早期就尽可能 “瘦身”,减少后续步骤的处理压力。这也是为什么我说,理解执行顺序是从 “会写 SQL” 到 “写好 SQL” 的分水岭 —— 前者只追求 “结果对”,后者还要追求 “效率高”。
最后,我想和你说:SQL 从来都不是一门 “死记硬背” 的语言,它是一门 “逻辑思维” 的语言。执行顺序的背后,是 MySQL 对数据处理的底层逻辑,也是我们对 “如何高效处理数据” 的思考方式。今天我们花了大量的篇幅拆解每一步的原理、误区和技巧,不是让你把这些内容背下来,而是希望你能建立起 “按执行顺序拆解 SQL” 的思维习惯。当你下次再面对一条复杂的 SQL 时,不要慌,把它拆成我们今天讲的 10 个步骤,一步一步分析数据的流转,你会发现,再复杂的 SQL,也不过是这 10 个步骤的组合。
学习的路上没有捷径,但有方法。执行顺序就是你学习 MySQL 的 “指南针”,无论你是刚入门的新手,还是已经有一定经验的开发者,理解并吃透它,都能让你在 SQL 的学习路上少走弯路。愿你能带着这份对 “底层逻辑” 的理解,去写每一行 SQL,去解决每一个业务问题,从 “照猫画虎” 到 “融会贯通”,最终能从容应对各种复杂的查询场景,让 SQL 成为你工作中得心应手的工具。毕竟,真正的高手,从来都不是记住了多少语法,而是理解了语法背后的逻辑 —— 而这,正是我们今天聊的全部意义。




