一条SQL的执行顺序
很多开发者按照自己"写SQL的顺序"理解SQL执行,但SQL的书写顺序和执行顺序完全不同。理解SQL真实的执行顺序,是掌握SQL优化、避免常见错误的关键。
一、核心口诀
从哪连什么,分组再筛选,选择去重排,最后限数量
|
# |
关键字 |
口诀 |
作用 |
|---|---|---|---|
|
1 |
FROM |
从哪 |
确定数据来源表,加载数据到内存 |
|
2 |
JOIN |
连什么 |
关联其他表,生成笛卡尔积 |
|
3 |
ON |
(连接条件) |
筛选连接条件,过滤无效连接行 |
|
4 |
WHERE |
什么 |
对原始数据进行行级过滤 |
|
5 |
GROUP BY |
分组 |
将过滤后的数据按指定列分组 |
|
6 |
HAVING |
再筛选 |
对分组后的结果再次过滤 |
|
7 |
SELECT |
选择 |
选取需要的列(此时才计算SELECT中的表达式和别名) |
|
8 |
DISTINCT |
去重 |
对结果集去重 |
|
9 |
ORDER BY |
排 |
对结果集排序(此时才可以使用SELECT中的别名) |
|
10 |
LIMIT |
最后限数量 |
截取指定行数返回 |
二、书写顺序 vs 执行顺序
2.1 你写的SQL顺序
2.2 实际执行顺序
最关键的认知:WHERE在GROUP BY之前执行,HAVING在GROUP BY之后执行。这就是为什么WHERE不能用聚合函数(此时还没分组计算),HAVING可以用聚合函数(此时已分组计算完毕)。
三、逐步详解
3.1 第一步:FROM — 确定数据来源
- 首先确定查询针对哪个表,数据库定位到该表的数据页
- 如有子查询,先执行子查询生成临时表,再作为FROM表
3.2 第二步:JOIN + ON — 表关联
- FROM表 与 JOIN表 做笛卡尔积(所有行的组合)
- ON条件筛选出真正匹配的行,不匹配的行不参与后续计算
- 注意:LEFT JOIN时,左表不匹配的行会被保留(右表字段填充NULL)
3.3 第三步:WHERE — 行级过滤
WHERE阶段不能使用SELECT中定义的别名!SELECT在WHERE之后执行,别名还不存在。同理,WHERE中不能使用聚合函数(COUNT/SUM/AVG等),因为此时尚未执行GROUP BY。
3.4 第四步:GROUP BY — 分组
- 将WHERE过滤后的数据按指定列分组
- 每个组会折叠成一行
- 未出现在GROUP BY中的非聚合列,SELECT不能直接引用(ONLY_FULL_GROUP_BY模式报错)
3.5 第五步:HAVING — 分组后过滤
- 对分组后的每个组进行过滤
- 可以使用聚合函数:HAVING COUNT(*) > 5 合法(此时已分组计算完毕)
- 性能建议:能用WHERE过滤的尽量用WHERE(在分组前缩小数据量);只有必须依赖聚合结果的才用HAVING
3.6 第六步:SELECT — 选取列
- 此时才选取需要输出的列,计算SELECT中的表达式和函数
- SELECT中定义的别名,在SELECT之后的ORDER BY/LIMIT中可以使用
- 聚合函数(SUM/COUNT/AVG/MAX/MIN)在这一步计算
3.7 第七步:DISTINCT — 去重
- 对SELECT选取的所有列组合进行去重
- DISTINCT作用于所有SELECT出的列,不能只对部分列去重
3.8 第八步:ORDER BY — 排序
- 对结果集进行排序
- 可以使用SELECT中定义的别名(因为SELECT已经执行)
- 排序可能需要filesort(文件排序),是常见性能瓶颈
3.9 第九步:LIMIT — 截取行数
- 在整个流程的最后,截取指定数量的行返回给客户端
- LIMIT m, n:跳过m行取n行(深度分页的根源)
四、实际案例演示
4.1 完整示例
4.2 数据流向模拟
可以看到数据量逐级减少——这就是为什么WHERE过滤越早越好,它决定了后面所有步骤要处理的数据量。
五、常见误区
|
误区 |
真相 |
|---|---|
|
WHERE中用了SELECT的别名 |
❌ 报错!SELECT在WHERE之后,别名还不存在 |
|
HAVING和WHERE可以互换 |
❌ WHERE在分组前,HAVING在分组后。WHERE中不能用聚合函数 |
|
ORDER BY中不能用别名 |
✅ 可以用!ORDER BY在SELECT之后,别名已存在 |
|
SELECT语句先执行 |
❌ SELECT是倒数第四步执行 |
记忆口诀再读一遍:从哪连什么,分组再筛选,选择去重排,最后限数量。理解了这个顺序,你对SQL的执行过程就有了清晰的心智模型。