SELECT语句逻辑执行顺序为:FROM→JOIN→WHERE→GROUP BY→HAVING→SELECT→ORDER BY→LIMIT;别名在SELECT阶段才生成,故WHERE中不可用;非聚合列须出现在GROUP BY中;HAVING过滤分组后结果;ORDER BY作用于最终投影列。

SELECT 语句的逻辑执行顺序不是从左到右
很多人写 SELECT a, b FROM t WHERE c > 10 ORDER BY a,就默认 SQL 是按这个书写顺序执行的——这是错的。实际数据库引擎先处理 FROM 和 JOIN 找出数据源,再用 WHERE 过滤行,然后才计算 SELECT 列表里的表达式(包括别名、函数、聚合等)。这意味着你不能在 WHERE 中直接引用 SELECT 里定义的别名,比如:SELECT price * 1.1 AS final_price FROM orders WHERE final_price > 100 会报错,因为 WHERE 阶段 final_price 还不存在。
GROUP BY 必须早于 SELECT,但晚于 WHERE
GROUP BY 是对 WHERE 过滤后的结果分组,再把每组压缩成一行;而 SELECT 中出现的非聚合列,必须出现在 GROUP BY 子句中(否则多数数据库如 PostgreSQL、SQL Server 会直接报错,MySQL 在严格模式下也一样)。常见错误是写:SELECT user_id, COUNT(*) FROM logs WHERE status = 'ok' SELECT user_id, COUNT(*) FROM logs WHERE status = 'ok' ——漏了 GROUP BY user_id,或者更隐蔽地,在 SELECT 里加了个 MAX(created_at) 却没意识到它依赖分组后计算。
-
WHERE过滤的是原始行,HAVING过滤的是分组后的聚合结果 -
GROUP BY字段可以是表达式(如YEAR(order_date)),但该表达式必须和SELECT中的非聚合项一致或可推导 - MySQL 5.7+ 默认开启
sql_mode=ONLY_FULL_GROUP_BY,不满足规则会直接报错,不是静默取任意值
ORDER BY 和 LIMIT 的位置影响性能与语义
ORDER BY 发生在 SELECT 计算之后、LIMIT 之前。这意味着排序对象是最终投影出的列(含别名、函数结果),所以 ORDER BY final_price 是合法的;但如果你在 WHERE 或 GROUP BY 阶段需要排序字段的索引加速,就得确保对应原始列有索引,而不是只给别名建索引(别名根本不会存进索引)。
-
LIMIT(或TOP/FETCH FIRST)是最晚执行的,但它会提前终止扫描——如果加了ORDER BY,数据库通常得先排序全部结果再截断;若没ORDER BY,可能随机返回几行 - 带
OFFSET的分页(如LIMIT 10 OFFSET 1000)在大数据集上性能差,因为仍要跳过前 1000 行,不是“跳过索引位置” - PostgreSQL 支持
TABLESAMPLE,MySQL 8.0+ 支持窗口函数配合ROW_NUMBER()实现更可控的分页,但本质仍是先生成完整序号再过滤
子查询和 CTE 的执行时机容易被误判
相关子查询(correlated subquery)会在外部查询的每一行上重复执行,比如:SELECT name, (SELECT COUNT(*) FROM orders o WHERE o.user_id = u.id) FROM users u,这里内层查询依赖外层的 u.id,无法提前物化。而 CTE(WITH 子句)在 PostgreSQL 和 SQL Server 中默认是“优化器可内联”的,不一定真生成临时结果集;只有显式加上 MATERIALIZED(PostgreSQL 12+)或用视图包装,才强制物化。MySQL 8.0 的 CTE 默认不物化,但递归 CTE 必须物化。
- 用
EXPLAIN看执行计划时,注意子查询是否显示为DEPENDENT SUBQUERY,这代表性能风险 - 把相关子查询改写成
LEFT JOIN+ 聚合,往往能显著提速 - CTE 名字只是语法糖,别以为它天然缓存结果——除非数据库明确支持物化且你启用了它
真正麻烦的不是记顺序,而是当优化器重排执行计划(比如下推谓词、提前聚合、索引跳扫)时,逻辑顺序和物理执行路径不再一一对应。这时候看 EXPLAIN ANALYZE 比背口诀管用得多。










