请注意:逻辑顺序用于理解语义与可用性(例如某些表达式为何不能出现在 WHERE 中),实际执行由优化器决定,可能重排或重写以获得更高性能。
三种“顺序”概念
- 书写顺序(Syntactic Order):你写在 SQL 里的顺序,如
SELECT ... FROM ... WHERE ... GROUP BY ... HAVING ... ORDER BY ... LIMIT ...。
- 逻辑处理顺序(Logical Order):SQL 标准定义的结果生成步骤,决定了哪些列/别名在某处可见。
- 物理执行顺序(Physical Plan):MySQL 优化器根据统计信息与索引生成的实际执行计划,可能与书写顺序不同。
典型 SELECT 的逻辑处理顺序(MySQL 8.0)
- WITH(CTE)
- FROM(表、子查询、视图、派生表)
- JOIN 与 ON(针对每个连接)
- WHERE(筛掉不满足条件的行)
- GROUP BY(形成分组)
- HAVING(基于聚合结果的分组过滤)
- 窗口函数(OVER 子句;在 MySQL 8.0 中作为选择列表/ORDER BY 可用的表达式)
- SELECT(计算选择列表表达式、列别名)
- DISTINCT(去重)
- UNION / UNION ALL(合并多 SELECT 的结果;最终 ORDER BY 通常作用于整体)
- ORDER BY(排序)
- LIMIT/OFFSET(截取行)
说明:
- 窗口函数不能用于
WHERE/GROUP BY/HAVING,通常在这些阶段之后、ORDER BY/LIMIT 之前(作为 SELECT 或 ORDER BY 中的表达式)求值。
DISTINCT 在选择列表计算后再去重。
ORDER BY 若在子查询中且结果未被外层使用排序,优化器可能移除(Order By elimination)。
各阶段要点
1) WITH(CTE)
- 在主查询之前生成临时结果集;MySQL 可能将简单 CTE 内联(merge),复杂或被多次引用的 CTE可能被物化。
- 递归 CTE:先执行锚成员,再迭代递归成员直至不产生新行。
2) FROM
- 确定参与查询的数据源:基本表、视图、子查询(派生表)。
- 子查询与视图在此阶段逻辑上“展开”。优化器可能选择物化或内联。
3) JOIN 与 ON
- 对每个连接应用其
ON 条件。
- 外连接注意:
ON 是连接条件;对于 LEFT/RIGHT OUTER JOIN,ON 过滤不丢弃保留侧(左/右)行。
WHERE 在连接之后再过滤,可能把外连接“变窄”甚至退化为内连接(若过滤了保留侧的 NULL 行)。
4) WHERE
- 行级过滤,不能使用聚合或窗口函数。
- 允许使用可计算的列与常量表达式;列别名在此不可见(因为别名在 SELECT 才生成)。
5) GROUP BY
- 基于分组键形成组;组内才能使用聚合函数(
COUNT/SUM/AVG/...)。
- MySQL 中非完全依赖分组键的选择列需使用聚合或基于功能依赖(仅在特定 SQL 模式下可能放宽,建议遵循标准写法)。
6) HAVING
- 针对分组后的结果过滤,允许使用聚合。
- 不可使用窗口函数;列别名在此可见(因发生在 SELECT 之前的逻辑争议,MySQL 实现允许在 HAVING 引用选择列表别名,但更稳妥是直接写表达式或聚合)。
7) 窗口函数(OVER)
- 在 MySQL 8.0 中,窗口函数只能出现在
SELECT 或 ORDER BY 表达式中。
- 逻辑上在分组与 HAVING 之后求值;不能出现在
WHERE/GROUP BY/HAVING。
- 可基于
PARTITION BY 和 ORDER BY 定义窗口。
8) SELECT
- 计算选择列表表达式并生成列别名。
- 此时可使用聚合或窗口函数的结果。
9) DISTINCT
10) UNION / UNION ALL
- 每个子 SELECT 按上述顺序独立处理,然后:
UNION ALL:连接结果(不去重)。
UNION:连接并去重(相当于在合并后应用 DISTINCT)。
- MySQL 的
ORDER BY/LIMIT 若作用于整个 UNION,需写在 UNION 语句末尾;单个分支的 ORDER BY/LIMIT 仅对该分支有效。
11) ORDER BY
- 对最终结果排序;
- 可使用选择列表别名与窗口函数表达式。
12) LIMIT/OFFSET
- 在排序后截取指定区间的行;若无排序,优化器可能使用索引或其它方式快速提取。
常见语义差异与注意事项
- 别名可见性:
WHERE 阶段不可引用 SELECT 别名;
ORDER BY 阶段可以引用 SELECT 别名。
- 外连接过滤:
- 将条件写在
ON 与写在 WHERE 的效果,在 OUTER JOIN 下可能不同。WHERE 可能过滤掉填充的 NULL,从而改变连接语义。
- 聚合与非聚合列:
SELECT 中出现非分组列时,必须使用聚合或符合功能依赖(建议总是明确聚合)。
- 窗口函数:
- 不能用于
WHERE/GROUP BY/HAVING。
- 常与聚合并列,但语义不同:窗口函数“不折叠行”(保留明细行),聚合“折叠为一行/每组一行”。
示例与解说
示例 1:典型查询
WITH active_customers AS (
SELECT id, name
FROM customers
WHERE status = 'active'
)
SELECT
c.id,
c.name,
SUM(o.amount) AS total_amount,
ROW_NUMBER() OVER (PARTITION BY c.id ORDER BY o.created_at) AS rn
FROM active_customers AS c
JOIN orders AS o
ON o.customer_id = c.id
WHERE o.created_at >= '2025-01-01'
GROUP BY c.id, c.name
HAVING SUM(o.amount) > 1000
ORDER BY total_amount DESC
LIMIT 10 OFFSET 0;
逻辑顺序讲解:
- 计算 CTE
active_customers;
FROM/JOIN 将 active_customers 与 orders 连接,ON 应用连接条件;
WHERE 过滤订单时间;
GROUP BY 按客户分组;
HAVING 基于聚合过滤分组;
- 计算选择列,包括聚合与窗口函数
ROW_NUMBER()(不能用于 WHERE/HAVING);
ORDER BY 使用别名 total_amount;
LIMIT 截取前 10 行。
示例 2:ON vs WHERE(外连接)
-- 将订单表左连到客户表,保留所有客户
SELECT c.id, o.id AS order_id
FROM customers c
LEFT JOIN orders o
ON o.customer_id = c.id AND o.status = 'paid' -- 在 ON 里筛选订单
WHERE c.region = 'EU'; -- WHERE 只过滤客户侧
- 若把
o.status = 'paid' 放到 WHERE,在 LEFT JOIN 下会过滤掉没有订单或未支付订单的客户(因为 o.* 为 NULL,不满足 WHERE),语义改变。
示例 3:窗口函数不能在 WHERE 中
-- 错误:窗口函数在 WHERE 中不允许
SELECT *
FROM t
WHERE ROW_NUMBER() OVER (ORDER BY id) <= 10;
-- 正确:在选择后使用别名或将窗口函数放子查询/CTE
WITH ranked AS (
SELECT
t.*,
ROW_NUMBER() OVER (ORDER BY id) AS rn
FROM t
)
SELECT *
FROM ranked
WHERE rn <= 10;
UNION 与 ORDER BY/LIMIT
SELECT id, name FROM a
UNION ALL
SELECT id, name FROM b
ORDER BY id
LIMIT 100;
- 两个分支各自完成逻辑处理后再合并;
- 末尾的
ORDER BY/LIMIT 作用于合并后的整体结果。
优化器可能进行的重写(不改变语义)
- 连接重排序(Join Reordering)与连接消除(Join Elimination)
- 谓词下推(Predicate Pushdown):将
WHERE/HAVING 条件尽量下推到更早阶段或更靠近数据源
- 子查询去相关化(De-correlation)、半连接(Semi-Join)与物化/内联选择
ORDER BY 消除:若排序对外层结果无影响,可能移除
- 使用索引访问路径、覆盖索引与回表优化
这些优化不改变逻辑语义,但会影响物理顺序与性能。
速查:你该把表达式放在哪里
- 过滤明细行:
WHERE
- 过滤分组:
HAVING
- 生成明细与聚合列:
SELECT
- 定义分组:
GROUP BY
- 定义连接条件:
ON
- 排序与分页:
ORDER BY + LIMIT/OFFSET
- 窗口统计(排名、累计、同组比较):
SELECT/ORDER BY 中的窗口函数
常见误区
- 在
WHERE 引用选择列表别名或窗口函数(不可行)
- 把分组过滤写在
WHERE(应写 HAVING)
- 在外连接中将保留侧的过滤条件写到
WHERE,改变了外连接语义
- 认为书写顺序等于执行顺序;优化器可以重排但不改变结果语义
参考
- MySQL 8.0 Manual:Queries, SELECT Syntax, Window Functions, WITH (CTE)
- SQL 标准与通用“Logical Query Processing”顺序
- 《High Performance MySQL》关于优化器与执行计划的章节