本文首发于「数据库干货铺」公众号,转载请联系授权
你是否曾写过复杂的SQL,却对结果感到困惑?比如明明加了 WHERE 条件,为什么 HAVING 还能“看到”聚合后的数据?又或者,为什么 SELECT 里定义的别名可以在 HAVING 中使用,却不能在 WHERE 中引用?
今天,我们就以一条真实业务场景中的SQL为例,彻底搞懂MySQL中SQL语句的逻辑执行顺序。这不是简单的语法罗列,而是带你穿透表象,理解数据库引擎如何一步步处理你的查询。
一、 从示例SQL谈起
先看一下以下这个SQL
SELECT DISTINCT
s.shop_name,
c.region,
SUM(o.amount) AS total_amount,
RANK() OVER (PARTITION BY s.shop_name ORDER BY SUM(o.amount) DESC) AS region_rank
FROM
shops s
JOIN
orders o ON s.shop_id = o.shop_id
JOIN
customers c ON o.customer_id = c.customer_id
WHERE
o.order_date >= '2026-01-01'
GROUP BY
s.shop_name, c.region
WITH ROLLUP
HAVING
total_amount > 1000
ORDER BY
s.shop_name IS NULL,
s.shop_name,
c.region IS NULL,
c.region
LIMIT 20 OFFSET 0;这条SQL包含了多个表连接、条件过滤、分组统计、窗口函数和排序分页。如果我们不了解MySQL的执行顺序,就很难理解为什么WHERE后面不能使用SELECT中定义的别名,而HAVING却可以。
1. SQL书写顺序 vs 执行顺序:天壤之别
大多数SQL编写者都遵循一种常见的书写顺序:SELECT -> FROM -> WHERE -> GROUP BY -> HAVING -> ORDER BY -> LIMIT。但这仅仅是书写习惯,并非MySQL的实际执行顺序!
MySQL的实际执行顺序是这样的:
FROM -> ON -> JOIN
-> WHERE -> GROUP BY
-> WITH ROLLUP -> HAVING
-> SELECT -> DISTINCT
-> ORDER BY -> LIMIT可以看到,MySQL的执行顺序与我们的书写顺序有显著差异。让我们通过一个直观的流程图来理解这一过程:

2. 深入解析MySQL执行流程
2.1 第一步:数据源的连接与过滤(FROM, ON, JOIN)
MySQL的执行过程从FROM子句开始。它首先对FROM子句中的表进行笛卡尔积操作,然后应用ON条件进行过滤。以我们的示例SQL为例:
FROM
shops s
JOIN
orders o ON s.shop_id = o.shop_id
JOIN
customers c ON o.customer_id = c.customer_idMySQL首先会加载shops、orders和customers三张表,生成一个包含所有可能组合的临时表(VT1)。然后应用ON条件过滤掉不匹配的记录,生成VT2。
实用技巧:当FROM子句包含多个表时,MySQL的执行顺序是从后往前、从右到左。因此,应该将数据量最小的表放在最后面,作为驱动表。
2.2 第二步:数据行过滤(WHERE)
WHERE子句在JOIN操作后执行,用于过滤掉不符合条件的行。在我们的例子中:
WHERE
o.order_date >= '2026-01-01'这一步会从中间表中筛选出2026年1月1日以后的订单记录。
注意: 由于WHERE子句在SELECT之前执行,因此不能在WHERE中使用SELECT中定义的列别名。这也是许多初学者容易犯错的地方。
2.3 第三步:数据分组(GROUP BY与WITH ROLLUP)
GROUP BY将数据按照指定的列进行分组,生成一个新的临时表。每组在结果集中只包含一行。在我们的例子中:
GROUP BY
s.shop_name, c.region
WITH ROLLUP这会先按店铺名称分组,再按区域分组。WITH ROLLUP选项会生成小计行(region为NULL)和总计行(shop_name和region都为NULL)。
关键点:从这一步开始,后续操作只能使用GROUP BY中的列或聚合函数。
2.4 第四步:分组结果过滤(HAVING)
HAVING用于对分组后的结果进行过滤,与WHERE类似,但它是在分组后执行,可以使用聚合函数。在我们的例子中:
HAVING
total_amount > 1000MySQL允许在HAVING中使用 SELECT列别名;这里使用了SELECT中定义的别名total_amount,所以可以使用别名。
2.5 第五步:选择与去重(SELECT与DISTINCT)
这是MySQL第一次处理SELECT子句,包括计算表达式、调用函数和生成列别名。随后,DISTINCT会去除重复行。需要注意的是,如果已经使用了GROUP BY,通常不需要再使用DISTINCT,因为分组后的每组数据已经是唯一的。
2.6 第六步:结果排序与分页(ORDER BY与LIMIT)
最后,ORDER BY对结果进行排序。由于它在SELECT之后执行,所以可以使用SELECT中定义的别名。在我们的例子中:
ORDER BY
s.shop_name IS NULL, -- 总计行排最后
s.shop_name,
c.region IS NULL, -- 小计行排在各商店末尾
c.region这种排序方式确保了小计和总计行出现在合适的位置。
最后,LIMIT子句用于限制返回的行数,完成整个查询过程
二、 MySQL的完整执行架构
除了上述SQL逻辑执行顺序外,了解MySQL的整体执行架构也很重要。总体的执行架构图如下(借用其他作者的图):

MySQL执行架构总体分为三层核心:连接层(接入)→ 服务层(处理逻辑)→ 存储引擎层(数据读写),最终与磁盘交互完成数据持久化。简化流程图如下:

各阶段核心作用如下:
MySQL与客户端的交互入口,支持多种协议(如 TCP/IP、Unix Socket、JDBC/ODBC 等),负责接收客户端的 SQL 请求并转发给连接器
验证客户端的用户名 / 密码、校验连接权限(如是否有权限连接该数据库)、维护连接(区分长连接 / 短连接,管理连接池)。如果是长连接,连接器会复用连接以减少认证开销,但需注意内存泄漏问题(可通过wait_timeout控制闲置连接超时)
缓存SELECT语句的结果(以SQL语句为Key,结果为Value),命中则直接返回,无需后续流程。MySQL8.0已彻底移除(因缓存失效频繁、维护成本高,实际命中率极低),5.x 版本也建议关闭(query_cache_type=0)
词法分析:把 SQL 语句拆分成最小单元(如关键字SELECT、表名、字段名、条件等),识别每个单元的含义
语法分析:校验 SQL 语法是否符合 MySQL 规范,若语法错误会直接返回报错(如You have an error in your SQL syntax);语法正确则生成抽象语法树(AST)
MySQL 的 “智能决策层”,核心是选择最优执行计划:例如:多表 JOIN 时选择哪个表作为驱动表、WHERE 条件中选择哪个索引、是否使用全表扫描 / 索引扫描等。优化器仅负责"选择计划",不执行SQL,执行计划可通过EXPLAIN命令查看
MySQL执行计划详解:从看不懂到秒懂,一线DBA的实战笔记
真正执行 SQL 的核心组件:首先校验该用户是否有操作目标表 / 字段的权限(若无则返回权限错误);然后调用对应存储引擎的 API(如 InnoDB 的read_row/write_row),而非直接操作数据;最后整理执行结果(如分页、排序)并返回给客户端
MySQL的“数据存储引擎”,是可插拔的架构(默认 InnoDB),负责数据的实际读写、事务管理、锁机制、索引维护等
存储MySQL的持久化数据,包括:数据文件(.ibd/.MYD)、日志文件(binlog/redo log/undo log)、配置文件等,存储引擎直接与磁盘交互完成数据读写
三、 结语
SQL执行顺序是MySQL核心原理之一,深入理解它有助于我们编写出更高效、更可靠的查询语句。下次当你编写复杂SQL时,不妨在脑海中过一遍MySQL的执行流程,相信你会更加得心应手!
希望本文对你理解MySQL的SQL执行顺序有所帮助。如果你有更好的技巧或疑问,欢迎留言讨论!
关注微信公众号「数据库干货铺」,获取更多数据库运维干货。