在MySQL中,查询语句的执行顺序并不是按照我们写SQL语句的顺序(SELECT、FROM、WHERE等)来执行的。了解实际执行顺序有助于我们理解查询逻辑和优化查询。
SQL语句的书写顺序:
- SELECT:选择要返回的列
- FROM:指定查询的表
- WHERE:对行进行过滤
- GROUP BY:对行进行分组
- HAVING:对分组后的组进行过滤
- ORDER BY:对结果进行排序
- LIMIT:限制返回的行数
SQL语句的实际执行顺序:
- FROM:首先确定查询的表,包括JOIN操作(如果有)
- ON:如果是JOIN,应用ON条件
- JOIN:执行连接操作
- WHERE:对行进行过滤,此时还没有分组,所以不能使用聚合函数
- GROUP BY:将数据按照指定的列分组
- HAVING:对分组后的组进行过滤,可以使用聚合函数(如COUNT, SUM等)
- SELECT:选择要返回的列,此时可以:
- 计算表达式
- 使用聚合函数(因为分组已经完成)
- 为列取别名(这些别名在后续步骤中可以被使用,但在WHERE和GROUP BY中不能使用)
- DISTINCT:去重(如果有DISTINCT关键字)
- ORDER BY:对结果集进行排序,可以使用SELECT中定义的别名
- LIMIT:限制返回的行数
SQL 逻辑执行顺序 (超详细版)
我们将以一个包含 JOIN 的复杂查询为例,逐一分解这个流水线上的每一个步骤。
查询的逻辑执行流程图
(原始表) --> 1. FROM/JOIN --> (VT1) --> 2. WHERE --> (VT2) --> 3. GROUP BY --> (VT3) --> 4. HAVING --> (VT4) --> 5. SELECT --> (VT5) --> 6. DISTINCT --> (VT6) --> 7. ORDER BY --> (VT7) --> 8. LIMIT --> (最终结果)
这里的 VT 代表虚拟表 (Virtual Table),它是上一步操作产生的结果集,作为下一步操作的输入。
步骤 1: FROM & JOIN (数据源的构建)
- 核心动作: 这是整个查询的起点。数据库会根据
FROM子句和JOIN子句,构建出本次查询所需要的数据源。 - 详细过程:
- 交叉连接 (Cross Join): 引擎首先会查看
FROM子句中的所有表。在逻辑上,它会先对这些表进行“笛卡尔积”,生成一个包含了所有表行组合的巨大虚拟表。例如,tableA有100行,tableB有100行,那么笛卡尔积就有 100 * 100 = 10000 行。 ON条件过滤: 接着,ON子句开始工作。它会遍历笛卡尔积产生的巨大虚拟表,逐行应用ON后面的连接条件进行过滤。只有满足ON条件的行才会被保留下来。这是将无意义的行组合剔除,只保留相关联数据的关键一步。- 添加外部行 (
OUTER JOIN): 如果你使用的是LEFT JOIN或RIGHT JOIN,在ON过滤之后,还会有一个附加步骤。以LEFT JOIN为例,引擎会检查左表中是否有任何行在ON过滤后没有在结果中找到匹配项。如果有,这些左表的行会被重新添加回结果集,其右表对应的列则用NULL值填充。RIGHT JOIN同理。
- 交叉连接 (Cross Join): 引擎首先会查看
- 产出: 一个包含了所有连接、筛选和(可能有的)
NULL填充的虚拟表,我们称之为 VT1。之后的所有操作都将基于这个 VT1 进行。 - 示例:
FROM employees e
LEFT JOIN departments d ON e.department_id = d.id
这里会先将 employees 和 departments 做笛卡尔积,然后用 e.department_id = d.id 筛选,最后把 employees 表里没有匹配到部门的员工行再加回来,d 表的列填充为 NULL。
步骤 2: WHERE (行级过滤器)
- 核心动作: 对 VT1 中的每一行数据进行过滤。
- 输入: 步骤 1 产生的虚拟表 VT1。
- 详细过程:
WHERE子句会逐行扫描 VT1,判断每一行是否满足WHERE后面的条件。如果条件为真 (TRUE),该行被保留;如果为假 (FALSE) 或未知 (UNKNOWN),该行被永久丢弃。 - 产出: 一个行数更少(或相等)的虚拟表 VT2。
- 示例:
SELECT `name` ,department FROM employees WHERE `name` = "张三";

- 关键限制: 因为
WHERE在SELECT之前执行,所以此时SELECT子句中定义的列别名是不可用的。你只能使用 VT1 中真实存在的列。
SELECT `name` as n ,department FROM employees WHERE n = "张三";

步骤 3: GROUP BY (数据分组)
- 核心动作: 将相似的行合并成一个摘要行。
- 输入: 步骤 2 产生的虚拟表 VT2。
- 详细过程:
GROUP BY子句会根据指定的列,将 VT2 中具有相同值的行分为一组。每个组在逻辑上会变成一行。 - 产出: 一个新的虚拟表 VT3,其行数等于 VT2 中不重复的分组数量。
- 示例:
SELECT * FROM employees GROUP BY department;

这是常见的错误,这个错误是由于 MySQL 的
sql_mode包含ONLY_FULL_GROUP_BY模式导致的,该模式要求SELECT列表中的非聚合列必须出现在GROUP BY子句中或具有函数依赖性
- 解决方案
-- 临时禁用(当前会话)
SET SESSION sql_mode=(SELECT REPLACE(@@sql_mode,'ONLY_FULL_GROUP_BY',''));
-- 永久禁用(需修改my.cnf配置文件)
[mysqld]
sql_mode=STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION

- 重要影响: 从这一步开始,查询的粒度从“单行”变为了“分组”。因此,后续的
SELECT和HAVING子句中,你只能引用:GROUP BY中使用的列。- 聚合函数(
COUNT(),SUM(),AVG(),MAX(),MIN()),因为它们是针对整个分组进行计算的。
步骤 4: HAVING (分组过滤器)
- 核心动作: 对
GROUP BY之后形成的分组进行过滤。 - 输入: 步骤 3 产生的虚拟表 VT3。
- 详细过程:
HAVING子句会遍历 VT3 中的每一个分组(摘要行),应用其后的条件。不满足条件的整个分组将被丢弃。 - 产出: 一个分组数量更少(或相等)的虚拟表 VT4。
- 示例:
-- 部门人数大于8的部门
SELECT department FROM employees GROUP BY department HAVING COUNT(name) > 8;

- 与 WHERE 的核心区别:
WHERE:过滤行,在分组前工作,不能使用聚合函数。HAVING:过滤分组,在分组后工作,可以(也通常会)使用聚合函数。
步骤 5: SELECT (最终列的投影)
- 核心动作: 计算并选择最终要显示的列。
- 输入: 步骤 4 产生的虚拟表 VT4。
- 详细过程: 这是数据库第一次,也是唯一一次处理
SELECT列表。它会执行以下操作:- 计算表达式:如
salary * 1.1。 - 调用函数:如
UPPER(name)或AVG(salary)。 - 生成列别名: 如此时
AVG(salary) AS average_salary中的average_salary才被创建。
- 计算表达式:如
- 产出: 一个新的虚拟表 VT5,包含了最终要展示的所有列和计算结果。
步骤 6: DISTINCT (结果去重)
- 核心动作: 移除结果集中的重复行。
- 输入: 步骤 5 产生的虚拟表 VT5。
- 详细过程: 如果使用了
DISTINCT关键字,数据库会扫描 VT5,并移除所有完全重复的行(即所有列的值都相同的行)。 - 产出: 一个行数可能更少的虚拟表 VT6。
步骤 7: ORDER BY (结果排序)
- 核心动作: 对最终的结果集进行排序。
- 输入: 步骤 6 产生的虚拟表 VT6。
- 详细过程:
ORDER BY子句会根据指定的列和排序规则(ASC或DESC)对 VT6 中的所有行进行排序。 - 产出: 一个行顺序确定的虚拟表 VT7。
- 关键点: 因为
ORDER BY在SELECT之后执行,所以SELECT中定义的列别名在这里是可用的,并且强烈推荐使用别名,这能让代码更清晰。
步骤 8: LIMIT / OFFSET (分页)
- 核心动作: 从已排序的结果集中,选取一个子集。
- 输入: 步骤 7 产生的虚拟表 VT7。
- 详细过程: 这是流水线的最后一步。
LIMIT指定了最多返回多少行,OFFSET指定了从第几行开始。 - 产出: 发送给客户端的最终结果集。
一点补充:关于查询优化器
上面描述的是逻辑执行顺序,这是一个概念模型,帮助我们理解 SQL 的行为和规则。而 MySQL 的查询优化器 在实际执行时,为了提高效率,可能会改变物理执行顺序。例如:
- 它可能会将
WHERE条件下推到存储引擎层,以便在读取数据时就进行过滤。 - 它可能会根据统计信息和索引,决定
JOIN的顺序(先连接哪个表)。
但是,优化器所做的任何改变都必须保证最终结果与逻辑执行顺序的结果完全一致。
总结表格(增强版)
|
逻辑顺序 |
关键字 |
输入虚拟表 |
核心动作 |
输出虚拟表 |
是否可用别名? |
|
1 |
|
原始表 |
构建数据源,通过 |
VT1 |
- |
|
2 |
|
VT1 |
过滤行 |
VT2 |
否 |
|
3 |
|
VT2 |
将行合并成分组 |
VT3 |
否 |
|
4 |
|
VT3 |
过滤分组 |
VT4 |
否 (MySQL有扩展支持) |
|
5 |
|
VT4 |
选择列,计算表达式,生成别名 |
VT5 |
- |
|
6 |
|
VT5 |
去除重复行 |
VT6 |
- |
|
7 |
|
VT6 |
对结果进行排序 |
VT7 |
是 |
|
8 |
|
VT7 |
选取最终的子集 |
最终结果 |
是 |
希望这次更详尽的、包含了 JOIN 和虚拟表概念的解释,能帮助您彻底理解 MySQL 语句的执行过程。

211

被折叠的 条评论
为什么被折叠?



