好的,我们来详细讲解一下 SQL 查询语句的执行顺序。
很多人会误以为 SQL 的执行顺序就是我们写的顺序(SELECT
-> FROM
-> WHERE
-> GROUP BY
-> HAVING
-> ORDER BY
),但实际上,数据库引擎在底层处理查询时,遵循一个完全不同的逻辑顺序。理解这个顺序对于编写高效、正确的 SQL 查询至关重要。
一、核心执行顺序(逻辑查询处理顺序)
这是 SQL 查询在数据库内部被处理的逻辑步骤。每一步都会产生一个虚拟表,作为下一步的输入。
我们以一个完整的查询为例:
SELECT DISTINCT column1, column2
FROM table1
JOIN table2 ON table1.id = table2.id
WHERE condition
GROUP BY column1
HAVING group_condition
ORDER BY column2 DESC
LIMIT n;
它的实际执行顺序如下:
第 1 步:FROM
/ JOIN
- 作用:确定需要查询的数据来源。数据库引擎首先会执行
FROM
子句,如果有关联的表(JOIN
),它会根据ON
条件将这些表连接起来,形成一个大的虚拟表(VT1)。 - 为什么是第一步:因为所有后续的操作(筛选、分组、排序)都必须基于一个完整的数据集。
第 2 步:WHERE
- 作用:对
FROM
步骤生成的虚拟表(VT1)进行行级过滤。根据WHERE
子句中的条件,移除不满足条件的行,生成一个新的虚拟表(VT2)。 - 关键点:
WHERE
子句在分组和聚合之前执行。因此,它不能使用聚合函数(如COUNT()
,SUM()
,AVG()
)。如果你尝试在WHERE
中使用聚合函数,数据库会报错。 - 性能提示:这是进行数据筛选最重要的环节,尽早过滤掉无用数据可以大大减少后续步骤的计算量。
第 3 步:GROUP BY
- 作用:根据
GROUP BY
子句中指定的列,将WHERE
步骤过滤后的虚拟表(VT2)中的数据进行分组。具有相同分组列值的行会被合并到一组,生成一个新的虚拟表(VT3)。 - 结果:此时的虚拟表由多个“组”构成,每一组代表一个唯一的分组键值。
第 4 步:HAVING
- 作用:对
GROUP BY
步骤生成的分组进行过滤。它类似于WHERE
,但作用于组而不是单行。根据HAVING
子句中的条件,移除不满足条件的组,生成一个新的虚拟表(VT4)。 - 关键点:
HAVING
在分组之后执行,因此它可以使用聚合函数(如HAVING COUNT(*) > 5
)。这是HAVING
和WHERE
最本质的区别。
第 5 步:SELECT
- 作用:这是最容易被误解的一步。直到这一步,数据库才开始真正“选择”需要返回的列。它会遍历
HAVING
步骤生成的虚拟表(VT4),并只保留SELECT
子句中明确指定的列(或表达式),生成一个新的虚拟表(VT5)。 - 关键点:
- 别名生效:在
SELECT
步骤中定义的列别名(如SELECT salary * 12 AS annual_salary
),在这一步之后才生效。这就是为什么在WHERE
或GROUP BY
中不能使用SELECT
中定义的别名,但在ORDER BY
中却可以的原因。 - 表达式计算:在
SELECT
中定义的表达式(如数学运算、函数调用)也是在这一步进行计算的。
- 别名生效:在
第 6 步:DISTINCT
- 作用:对
SELECT
步骤生成的虚拟表(VT5)进行去重。移除所有完全相同的行,生成一个新的虚拟表(VT6)。 - 性能:
DISTINCT
操作通常需要排序或哈希,可能会消耗较多资源,应谨慎使用。
第 7 步:ORDER BY
- 作用:对最终的结果集(
DISTINCT
步骤后的虚拟表 VT6)进行排序。根据ORDER BY
子句中指定的列和排序方式(ASC
或DESC
)对行进行排序。 - 关键点:
- 最后一步之一:
ORDER BY
是在几乎所有数据处理完成后才执行的。 - 可以使用别名:因为
ORDER BY
在SELECT
之后执行,所以它可以引用SELECT
中定义的列别名。
- 最后一步之一:
第 8 步:LIMIT
/ OFFSET
/ TOP
- 作用:这是整个查询的最后一步。它从排序好的结果集中,限制返回的行数。
LIMIT n
:返回前 n 行。OFFSET m LIMIT n
:跳过前 m 行,返回接下来的 n 行。TOP n
:返回前 n 行。
- 性能提示:
LIMIT
通常在分页查询中使用。但请注意,如果查询中包含了ORDER BY
,数据库需要先对所有符合条件的数据进行排序,然后再应用LIMIT
,这在数据量很大时可能会很慢。
二、执行顺序与书写顺序的对比
执行顺序 | 子句 | 作用描述 | 书写顺序 |
---|---|---|---|
1 | FROM , JOIN | 确定数据源,连接表 | 2 |
2 | WHERE | 过滤行(在分组前) | 3 |
3 | GROUP BY | 对行进行分组 | 4 |
4 | HAVING | 过滤组(在分组后) | 5 |
5 | SELECT | 选择列,计算表达式,定义别名 | 1 |
6 | DISTINCT | 去重 | 1 (在SELECT后) |
7 | ORDER BY | 对最终结果排序 | 6 |
8 | LIMIT | 限制返回的行数 | 7 |
三、为什么理解执行顺序很重要?(实例说明)
1. 为什么 WHERE
中不能用聚合函数,而 HAVING
可以?
-- 错误的写法
SELECT department, COUNT(*) as emp_count
FROM employees
WHERE COUNT(*) > 5 -- 报错!因为 WHERE 在 GROUP BY 之前执行,此时 COUNT(*) 还不存在
GROUP BY department;
-- 正确的写法
SELECT department, COUNT(*) as emp_count
FROM employees
GROUP BY department -- 先分组
HAVING COUNT(*) > 5; -- 再对组进行过滤
2. 为什么 ORDER BY
可以使用 SELECT
中的别名,而 WHERE
不可以?
-- 错误的写法
SELECT first_name, salary * 12 AS annual_salary
FROM employees
WHERE annual_salary > 60000; -- 报错!因为 WHERE 在 SELECT 之前执行,别名 'annual_salary' 还不存在
-- 正确的写法
SELECT first_name, salary * 12 AS annual_salary
FROM employees
WHERE salary * 12 > 60000; -- 在 WHERE 中重复写表达式
-- 或者,利用执行顺序,在 ORDER BY 中使用别名
SELECT first_name, salary * 12 AS annual_salary
FROM employees
WHERE salary > 5000 -- 先用原始列过滤
ORDER BY annual_salary DESC; -- 再用别名排序(因为 ORDER BY 在 SELECT 之后)
总结
记住这个核心顺序:FROM/JOIN → WHERE → GROUP BY → HAVING → SELECT → DISTINCT → ORDER BY → LIMIT。
将 SQL 的执行顺序想象成一条流水线,数据从源头(FROM
)开始,经过一道道工序(WHERE
, GROUP BY
等)的加工和筛选,最终形成我们想要的产品(结果集)。理解了这个流程,你就能更清晰地思考问题,写出逻辑正确、性能更优的 SQL 查询。
SELECT 中的非聚合列必须出现在 GROUP BY 子句中,否则会报错。
HAVING 子句用于过滤聚合函数的结果(如 SUM、COUNT、AVG 等)。
g.weight < 50 是对原始列的过滤,不属于聚合条件,应该放在 WHERE 子句中。