MySQL中SELECT语句的执行顺序
本文讲解MySQL SELECT语句的逻辑执行顺序,区别于书写顺序。它从FROM开始,经WHERE过滤行,GROUP BY分组,HAVING过滤组,再到SELECT选列和ORDER BY排序,最后由LIMIT截取。
这是一个非常核心且重要的MySQL知识点。理解SELECT语句的执行顺序,对于编写高效、正确的SQL查询至关重要。
我们书写SQL的顺序和MySQL实际执行的顺序是不一样的。
书写顺序
我们通常是按照这个顺序来写一个完整的SELECT查询:
sql
SELECT [DISTINCT] select_list
FROM table_references
[JOIN table_references ON join_condition]
WHERE where_condition
GROUP BY group_by_list
HAVING having_condition
ORDER BY order_by_list
LIMIT offset, row_count;
逻辑执行顺序(重点)
MySQL(以及大多数关系型数据库)会按照下面的逻辑顺序来解析和执行这个查询。你可以把它想象成一个数据处理的流水线,数据一步步地被筛选和加工。
FROM:确定数据来源。- 执行的第一步是找到查询的表。如果涉及到
JOIN,会根据ON条件将多个表的数据连接起来,形成一个庞大的“虚拟表”(Virtual Table)。这个虚拟表包含了所有原始表符合连接条件的行和列。
- 执行的第一步是找到查询的表。如果涉及到
WHERE:行级过滤。- 在第一步生成的虚拟表上,应用
WHERE子句中的条件。它逐行检查,只保留那些让WHERE条件为TRUE的行。 - 关键点:
WHERE在数据分组(GROUP BY)之前执行,所以你不能在WHERE子句中使用聚合函数(如COUNT(),SUM(),AVG())。
- 在第一步生成的虚拟表上,应用
GROUP BY:数据分组。- 将通过
WHERE筛选后的结果集,按照GROUP BY子句指定的列进行分组。具有相同值的行会被分到同一个组里。 - 执行完这一步后,后续操作的对象就不再是单条记录,而是整个“分组”了。
- 将通过
HAVING:分组过滤。- 对
GROUP BY之后形成的分组进行过滤。只有满足HAVING子句中条件的分组才会被保留下来。 - 关键点:
HAVING在数据分组之后执行,所以你可以在HAVING子句中使用聚合函数。这正是它与WHERE的核心区别。
- 对
SELECT:选择列/计算表达式。- 到了这一步,MySQL才真正开始处理
SELECT后面跟着的列了。它会从保留下来的分组数据中,选出你指定的列,或者计算SELECT列表中的表达式(比如CONCAT(),AVG(score)等)。 - 关键点:因为
SELECT在GROUP BY之后执行,所以SELECT列表中既可以包含分组列,也可以包含聚合函数。同时,为列指定的别名(Alias)也是在这一步生成的。
- 到了这一步,MySQL才真正开始处理
DISTINCT:去除重复行。- 如果查询中包含了
DISTINCT关键字,那么在SELECT选出所有列之后,会移除结果集中完全重复的行。
- 如果查询中包含了
ORDER BY:排序。- 对最终的结果集按照
ORDER BY子句指定的列进行排序。 - 关键点:因为
ORDER BY在SELECT之后执行,所以你可以在ORDER BY子句中使用SELECT中定义的别名。
- 对最终的结果集按照
LIMIT:限制返回行数。- 这是整个流水线的最后一步。在结果集已经排序完成后,
LIMIT会从中选取指定范围的行返回给客户端。
- 这是整个流水线的最后一步。在结果集已经排序完成后,
直观的总结对比
| 序号 | 逻辑执行顺序 | SQL书写顺序 | 作用说明 |
|---|---|---|---|
| 1 | FROM / JOIN |
FROM |
从哪个表获取数据,并进行连接,形成虚拟表。 |
| 2 | WHERE |
WHERE |
对虚拟表中的行进行过滤。 |
| 3 | GROUP BY |
GROUP BY |
将行分组。 |
| 4 | HAVING |
HAVING |
对分组进行过滤。 |
| 5 | SELECT |
SELECT |
选择要显示的列或表达式,并生成别名。 |
| 6 | DISTINCT |
DISTINCT |
对 SELECT 后的结果去重。 |
| 7 | ORDER BY |
ORDER BY |
对最终结果集进行排序。 |
| 8 | LIMIT |
LIMIT |
从排序后的结果集中,取出指定数量的行。 |
一个实例贯穿全程
假设我们有 students 和 scores 两张表。
sql
-- 查询每个科目平均分超过75分的,并且学生人数超过10人的科目,
-- 按平均分降序排列,只显示科目名称和平均分。
SELECT
s.subject AS '科目', -- 第 5 步: 选择列并应用别名
AVG(s.score) AS '平均分' -- 第 5 步: 计算聚合函数并应用别名
FROM
scores s -- 第 1 步: 确定主表 scores 并起别名 s
JOIN
students st ON s.student_id = st.id -- (假设有JOIN)
WHERE
st.grade = '三年级' -- 第 2 步: 过滤行,只保留三年级的学生成绩
GROUP BY
s.subject -- 第 3 步: 按科目进行分组
HAVING
AVG(s.score) > 75 -- 第 4 步: 过滤分组,只保留平均分 > 75 的科目
AND COUNT(s.student_id) > 10 -- 第 4 步: 过滤分组,只保留人数 > 10 的科目
ORDER BY
`平均分` DESC -- 第 7 步: 按 SELECT 中定义的别名 `平均分` 降序排序
LIMIT 10; -- 第 8 步: 取排序后的前10条记录
为什么理解这个顺序很重要?
- 别名的使用:你不能在
WHERE子句中使用SELECT中定义的列别名,因为WHERE在SELECT之前执行,此时别名还不存在。但你可以在ORDER BY中使用,因为ORDER BY在SELECT之后执行。 - 聚合函数的位置:你不能在
WHERE中使用聚合函数(如COUNT(*)),因为WHERE操作的是行,还没有进行分组。聚合函数必须用在HAVING或SELECT列表、ORDER BY子句中。 - 查询优化:了解执行顺序有助于你写出更高效的SQL。例如,尽量用
WHERE提前过滤掉大量无关数据,这样后续的GROUP BY和其他操作处理的数据量就会大大减少,从而提升性能。