搞懂这3种常见SQL语句顺序,才算真正会写SQL!
搞懂这3种常见SQL语句顺序,才算真正会写SQL!
ZhangCurry搞懂这3种常见SQL语句顺序,才算真正会写SQL!
我们为什么需要理解SQL语句的执行顺序?理解SQL语句在数据库内部的真实执行流程,对我们写出高效、正确、可维护的SQL查询语句非常重要。SQL(Structured Query Language)是关系型数据库的标准查询语言,语法结构看似线性排列(如:SELECT ... FROM ... WHERE ... GROUP BY ... HAVING ... ORDER BY ... LIMIT),实际执行顺序与书写顺序并不一致。不同数据库(如:MySQL、PostgreSQL、SQL Server、Oracle)虽然在语法细节和优化器实现上存在差异,但SQL语句实际执行顺序的基本逻辑一致。我们只要掌握这一实际执行顺序的基本逻辑,再搞清楚各数据库的特性差异,就能让我们在跨库SQL、性能调优、错误排查等方面得心应手。
一、通用SQL语句执行顺序(逻辑模型)
无论我们使用哪种关系型数据库,一条标准SELECT查询语句的逻辑执行顺序(Logical Processing Order)如下:
1 | 1、FROM/JOIN |
我们要注意:这是“逻辑执行顺序”,不是“实际(物理)执行顺序”。真实执行时,数据库优化器可能根据索引、统计信息、成本估算等调整物理执行路径,但最终结果可认为等价于按此逻辑执行顺序执行。
下面我们对每个步骤进行详细说明:
1、FROM/JOIN,确定数据源
这是我们SQL查询的第一步。数据库引擎从FROM子句指定的表或视图中读取数据。如果存在JOIN,则在此阶段完成表连接操作(如:INNER JOIN, LEFT JOIN等等)。
- 作用:构建初始数据集(虚拟表)。
- 可访问内容:表中的所有列。
- 不可访问内容:
SELECT中定义的别名、聚合函数结果。
2、WHERE,行级过滤
对FROM阶段生成的数据集进行条件过滤,仅保留满足WHERE条件的行。
- 作用:减少后续处理的数据量,提高效率。
- 可访问内容:原始表列、连接后的列。
- 不可访问内容:
- 聚合函数(如:
COUNT(),SUM())→ 因为尚未分组。 SELECT中定义的列别名 → 因为SELECT尚未执行。
- 聚合函数(如:
正确写法:
1 | WHERE salary > 50000 |
错误写法:
1 | WHERE COUNT(*) > 5 -- 聚合函数不能在WHERE中使用 |
3、GROUP BY,分组聚合
将WHERE过滤后的数据按指定列进行分组。每组生成一行结果,通常配合聚合函数(如:COUNT, SUM, AVG, MAX, MIN)使用。
- 作用:将多行数据“折叠”为一行,用于统计分析。
- 可访问内容:
WHERE后剩余的列。 - 限制:
SELECT中的非聚合列,若不依赖于GROUP BY列的函数依赖(如:主键/唯一键),则必须出现在GROUP BY子句中(SQL标准;MySQL默认宽松模式不强制,需通过sql_mode=ONLY_FULL_GROUP_BY启用标准行为)。- 不能使用
SELECT别名(因为SELECT在其后执行)。
4、HAVING,组级过滤
对GROUP BY分组后的结果进行过滤,仅保留满足HAVING条件的组。
- 作用:弥补
WHERE无法使用聚合函数的缺陷。 - 可访问内容:
GROUP BY的分组列。- 聚合函数(如:
COUNT(*) > 5)。 - 部分数据库支持
SELECT别名(如:MySQL),但非标准。
正确写法:
1 | HAVING COUNT(*) > 2 |
错误写法(在严格数据库中):
1 | HAVING emp_count > 2 -- emp_count是SELECT中定义的别名(PostgreSQL/SQL Server不支持) |
5、SELECT,投影列
选择最终要返回的列,包括:原始列、计算列、聚合函数、别名定义等。
- 作用:定义输出结构。
- 可访问内容:
- 所有经过
GROUP BY的列。 - 聚合函数结果。
- 表达式计算(如:
salary * 1.1 AS new_salary)。
- 所有经过
- 我们注意看:此时才“正式”定义列别名,后续步骤(
ORDER BY,LIMIT)可使用。
6、DISTINCT,去重
对SELECT的结果集进行去重,去除完全相同的行。
- 作用:确保结果唯一性。
- 执行位置:我们简单讲:在
SELECT之后,ORDER BY之前。我们啰嗦讲:DISTINCT逻辑上基于SELECT投影的列去重,执行时机可由优化器根据ORDER BY的列选择“先去重后排序”或“先排序后去重”,但最终结果等价于“先按SELECT列去重,再按ORDER BY排序”。 - 性能影响:可能导致排序或哈希操作,大数据集时,我们要慎用。
7、ORDER BY,排序
对最终结果集按指定列或表达式进行排序(升序ASC或降序DESC)。
- 作用:控制结果展示顺序。
- 可访问内容:
SELECT中的列名。SELECT中定义的别名(我们推荐使用)。- 位置编号(如:
ORDER BY 1, 2,我们不推荐,可读性差)。
- 我们要注意:排序是资源密集型操作,我们尽量利用索引避免文件排序(filesort)。
8、LIMIT/OFFSET/TOP/FETCH,分页限制
限制返回的行数,常用于分页。
- 作用:控制结果集大小。
- 执行位置:最后一步。
- 语法差异:
- MySQL/PostgreSQL:
LIMIT n OFFSET m - SQL Server:
OFFSET m ROWS FETCH NEXT n ROWS ONLY - Oracle 12c+:
FETCH FIRST n ROWS ONLY - Oracle 11g-:需嵌套
ROWNUM - SQL Server(旧):
TOP n
- MySQL/PostgreSQL:
二、各数据库实际执行差异
虽然“逻辑执行顺序”一致,但不同数据库在语法支持、优化器行为、执行计划生成等等方面存在显著差异。下面我们以MySQL、PostgreSQL、SQL Server、Oracle为例进行对比。
1、MySQL特性差异:
别名支持灵活:
1
SELECT col1 + col2 AS total FROM t HAVING total > 100; -- 支持
我们看说明:MySQL在非严格模式(或ONLY_FULL_GROUP_BY关闭时)支持HAVING使用SELECT别名;启用ONLY_FULL_GROUP_BY后,仅当别名对应的表达式无歧义(如非聚合列)时支持,聚合函数别名仍需显式写完整表达式。
LIMIT优化:如果
ORDER BY列有索引,LIMIT可能只排序前N行,极大提升性能。1
SELECT * FROM users ORDER BY created_at DESC LIMIT 10;
子查询优化(8.0+):早期版本子查询性能差,8.0+支持CTE、窗口函数、子查询物化/合并优化。
GROUP BY宽松模式:默认允许
SELECT出现非GROUP BY列(返回不确定值),我们可通过sql_mode=ONLY_FULL_GROUP_BY禁用。
2、PostgreSQL特性差异:
CTE优化屏障:PostgreSQL 11及以下默认将
CTE视为优化屏障(独立执行,类似临时表);PostgreSQL 12及以上默认支持CTE内联优化(无副作用时与主查询合并),可通过MATERIALIZED/NOT MATERIALIZED显式控制。1
2WITH cte AS (SELECT * FROM large_table WHERE condition)
SELECT * FROM cte JOIN other_table ... -- CTE 独立执行DISTINCT ON:独有语法,按指定列去重并保留“第一行”(需配合
ORDER BY)。1
2
3SELECT DISTINCT ON (department) name, salary, department
FROM employees
ORDER BY department, salary DESC; -- 每个部门工资最高者HAVING严格:不支持在
HAVING中使用SELECT别名,我们必须写完整表达式。1
2HAVING COUNT(*) > 5 -- 必须写完整表达式
HAVING emp_count > 5 -- 不支持(除非emp_count是分组列)
3、SQL Server特性差异:
分页语法:使用标准
OFFSET ... FETCH(2012+),旧版用TOP。1
2
3SELECT * FROM t
ORDER BY id
OFFSET 10 ROWS FETCH NEXT 20 ROWS ONLY;APPLY运算符:支持
CROSS APPLY/OUTER APPLY,用于关联表值函数或子查询。1
2
3
4
5SELECT d.name, e.*
FROM departments d
CROSS APPLY (
SELECT TOP 3 * FROM employees WHERE dept_id = d.id ORDER BY salary DESC
) e;TOP + ORDER BY强制绑定:我们使用
TOP时,若无ORDER BY,结果顺序不确定。1
2SELECT TOP 10 * FROM t ORDER BY id; -- 必须有ORDER BY
SELECT TOP 10 * FROM t; -- 顺序不确定优化器强大:Cardinality Estimator对复杂查询、统计信息利用更精准。
4、Oracle特性差异:
ROWNUM陷阱:
ROWNUM是伪列,在当前查询层级的WHERE过滤后、ORDER BY之前分配;若需按指定顺序分页,必须先将ORDER BY放入子查询,让ROWNUM作用于排序后的结果集(避免分页混乱)。1
2
3
4
5
6
7-- 错误:先分配ROWNUM,再排序,结果混乱
SELECT * FROM employees WHERE ROWNUM <= 10 ORDER BY salary DESC;
-- 正确:先排序,再用ROWNUM
SELECT * FROM (
SELECT * FROM employees ORDER BY salary DESC
) WHERE ROWNUM <= 10;12c+分页语法:支持标准
FETCH FIRST n ROWS ONLY。1
SELECT * FROM employees ORDER BY salary DESC FETCH FIRST 10 ROWS ONLY;
执行计划稳定性:支持SQL Plan Baseline、SQL Profile,可固化执行计划,避免因统计信息变化导致性能抖动。
谓词推进(Predicate Pushing):优化器常将子查询条件“推”到外层,减少数据扫描。
5、各数据库特性差异对比表
| 特性 | MySQL |
PostgreSQL |
SQL Server |
Oracle |
|---|---|---|---|---|
| 分页语法 | LIMIT n OFFSET m |
LIMIT n OFFSET m |
OFFSET m FETCH NEXT n |
ROWNUM(旧) / FETCH(12c+) |
CTE优化 |
可合并 | 默认不合并(屏障) | 可合并 | 可合并 |
HAVING用别名 |
支持 | 不支持 | 不支持 | 不支持 |
| 特有语法 | — | DISTINCT ON |
APPLY |
ROWNUM, CONNECT BY |
| 子查询优化 | 8.0+大幅改进 | 强(转LATERAL JOIN) |
强(转JOIN) |
强(谓词推进) |
| 分组宽松性 | 默认宽松 | 严格 | 严格 | 严格 |
三、分步执行演示
为了直观理解执行顺序,我们使用统一模拟数据,逐步展示各阶段中间结果。
1、模拟数据表:employees
1 | CREATE TABLE employees ( |
2、查询示例:多条件分组+过滤+排序+分页
1 | SELECT |
3、分步执行过程(逻辑顺序)
步骤1:FROM+WHERE,过滤原始行
1 | -- WHERE hire_year >= 2020 |
保留8行:
| id | name | dept | gender | salary | hire_year |
|---|---|---|---|---|---|
| 2 | Bob | HR | M | 65000 | 2020 |
| 3 | Charlie | HR | M | 70000 | 2021 |
| 4 | David | Engineering | M | 80000 | 2020 |
| 5 | Eve | Engineering | F | 85000 | 2020 |
| 6 | Frank | Engineering | M | 90000 | 2021 |
| 7 | Grace | Engineering | F | 95000 | 2022 |
| 9 | Ivy | Sales | F | 60000 | 2021 |
| 10 | Jack | Sales | M | 65000 | 2022 |
步骤2:GROUP BY,多级分组(department → gender)
1 | GROUP BY department, gender |
分组层级:
- 第一层:按
department→ HR, Engineering, Sales - 第二层:各组内按
gender→ M, F
分组结果(含聚合):
| department | gender | emp_count | avg_salary |
|---|---|---|---|
| HR | M | 2 | 67500.00 |
| Engineering | M | 2 | 85000.00 |
| Engineering | F | 2 | 90000.00 |
| Sales | F | 1 | 60000.00 |
| Sales | M | 1 | 65000.00 |
步骤3:HAVING,过滤分组
1 | HAVING COUNT(*) >= 2 |
保留3组:
| department | gender | emp_count | avg_salary |
|---|---|---|---|
| HR | M | 2 | 67500.00 |
| Engineering | M | 2 | 85000.00 |
| Engineering | F | 2 | 90000.00 |
步骤4:SELECT,定义输出列和别名
1 | SELECT department, gender, COUNT(*) AS emp_count, AVG(salary) AS avg_salary |
输出结构确定,别名emp_count和avg_salary生效。
步骤5:ORDER BY,排序
1 | ORDER BY department, avg_salary DESC |
排序后:
| department | gender | emp_count | avg_salary |
|---|---|---|---|
| Engineering | F | 2 | 90000.00 |
| Engineering | M | 2 | 85000.00 |
| HR | M | 2 | 67500.00 |
步骤6:LIMIT,取前2行
1 | LIMIT 2 |
最终结果:
| department | gender | emp_count | avg_salary |
|---|---|---|---|
| Engineering | F | 2 | 90000.00 |
| Engineering | M | 2 | 85000.00 |
四、WHERE、GROUP BY、HAVING、ORDER BY执行顺序详解
这四个子句属于SQL查询的基本架构部分,其执行顺序和作用范围常被我们混淆。我们用一张表来总结:
| 子句 | 执行阶段 | 作用对象 | 可用内容 | 不可用内容 | 典型错误 |
|---|---|---|---|---|---|
WHERE |
第一 | 原始行 | 表列、常量、表达式 | 聚合函数、SELECT别名 |
WHERE COUNT(*) > 5 |
GROUP BY |
第二 | WHERE后数据 |
列名 | SELECT别名、未分组列* |
SELECT name, COUNT(*) GROUP BY dept |
HAVING |
第三 | 分组后“组” | 分组列、聚合函数 | 未分组列、部分别名 | HAVING name = 'Alice' |
ORDER BY |
第四 | 最终结果集 | SELECT列、别名、位置编号 |
无(最自由) | 无 |
我们要注意:MySQL默认允许SELECT未分组列,但值不确定,我们不推荐。
经典误区:WHERE vs HAVING
我们很多人误以为HAVING只是WHERE的“替代品”,其实二者作用阶段完全不同:
WHERE→ 分组前过滤行 → 减少参与分组的数据量 → 高效HAVING→ 分组后过滤组 → 无法减少分组计算量 → 相对低效
正确用法:
1 | -- 查询平均工资 > 70000的部门 |
五、多条件GROUP BY执行详解
当GROUP BY包含多个列时,分组是层级嵌套的,顺序至关重要。
1、示例:GROUP BY A, B与GROUP BY B, A的区别
查询1:GROUP BY department, gender
1 | SELECT department, gender, COUNT(*) |
结果:
| department | gender | COUNT |
|---|---|---|
| HR | M | 2 |
| Engineering | M | 2 |
| Engineering | F | 2 |
| Sales | F | 1 |
| Sales | M | 1 |
→ 先按部门分,部门内再按性别分。
查询2:GROUP BY gender, department
1 | SELECT gender, department, COUNT(*) |
结果:
| gender | department | COUNT |
|---|---|---|
| M | HR | 2 |
| M | Engineering | 2 |
| M | Sales | 1 |
| F | Engineering | 2 |
| F | Sales | 1 |
→ 先按性别分,性别内再按部门分。
虽然数据相同,但我们能看出分组结构和展示顺序完全不同,适用于不同分析视角。严格地说:GROUP BY A,B与GROUP BY B,A的分组逻辑不同(层级顺序相反),结果集的“默认展示顺序”可能不同,但结果集结构(列定义)一致,最终展示顺序需通过显式ORDER BY控制。
2、“多条件分组”正确、高效、不容易出错的写法
(1)高频筛选列放前面:如果我们常按department筛选,应放GROUP BY首位。
(2)利用索引:多列分组时,联合索引(col1, col2)可加速GROUP BY col1, col2。
(3)避免过度分组:分组列越多,组数越多,聚合开销越大。
(4)SELECT必须包含所有非聚合列:严格模式下,SELECT中非聚合列我们必须全部放在GROUP BY中。
六、子查询执行顺序与优化差异
子查询(Subquery)的执行顺序受数据库优化器影响极大,不同数据库引擎处理策略不同。
1、示例:关联子查询
1 | SELECT name, salary, department |
逻辑:我们要查询工资高于本部门平均工资的员工。
2、各数据库优化方法
| 数据库 | 优化方法 | 执行方式 | 性能特点 |
|---|---|---|---|
MySQL 5.7- |
相关子查询 → 逐行执行 | 对外层每行执行一次子查询 | 慢(O(n*m)) |
MySQL 8.0+ |
子查询合并 → 转为JOIN |
半连接或内连接 | 快 |
PostgreSQL |
转为LATERAL JOIN |
类似关联连接 | 快,可读性强 |
SQL Server |
优化器选择 → 常转为JOIN |
利用统计信息选最优计划 | 快,执行计划灵活 |
Oracle |
谓词推进 + 子查询合并 | 条件推外层,减少扫描 | 快,企业级优化 |
我们看说明:MySQL 8.0+大幅改进子查询优化,支持非关联子查询的物化/合并(转为JOIN),但关联子查询仍可能逐行执行,复杂场景下建议手动改写为JOIN以确保性能。
3、如何查看实际执行计划?
- MySQL:
EXPLAIN FORMAT=JSON SELECT ... - PostgreSQL:
EXPLAIN (ANALYZE, BUFFERS) SELECT ... - SQL Server:
SET STATISTICS IO ON; SELECT ...+ 执行计划图形 - Oracle:
EXPLAIN PLAN FOR SELECT ...; SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);
通过执行计划,我们可看到子查询是否被“扁平化”为JOIN,避免性能陷阱。
七、窗口函数(Window Functions)的执行时机与影响
窗口函数允许在不破坏原始行结构的前提下进行复杂的计算,如:排名、移动平均、累计求和等。窗口函数的引入改变了我们对传统的执行流程理解,其执行时机介于SELECT和ORDER BY之间,是在逻辑执行顺序中的后半段参与进来的。
1、窗口函数的执行位置
在标准的SQL逻辑执行顺序中,窗口函数属于SELECT阶段的一部分,执行时机分两种:
- 若OVER()包含ORDER BY:先执行窗口内排序,再计算窗口函数;
- 若OVER()无ORDER BY:在非窗口表达式(普通列、聚合函数)之后计算;
两种情况均在DISTINCT之前完成。
1 | ... |
我们看关键点:窗口函数可以引用SELECT中定义的别名,但不能在WHERE、GROUP BY或HAVING中使用,因为这些阶段早于SELECT。
2、示例:窗口函数在分组后计算排名
1 | SELECT |
执行流程解析:
1、FROM + WHERE:筛选hire_year >= 2020的员工(8人)
2、GROUP BY:本例无GROUP BY,跳过
3、HAVING:无,跳过
4、SELECT:
- 先计算普通列:
department,name,salary - 再计算窗口函数:
dept_avg:按部门分区计算平均工资rank_in_dept:按部门内工资降序排名
5、DISTINCT:无,跳过
6、ORDER BY:按department和rank_in_dept排序输出
输出示例:
| department | name | salary | dept_avg | rank_in_dept |
|---|---|---|---|---|
| Engineering | Grace | 95000 | 87500.00 | 1 |
| Engineering | Frank | 90000 | 87500.00 | 2 |
| Engineering | Eve | 85000 | 87500.00 | 3 |
| Engineering | David | 80000 | 87500.00 | 4 |
| HR | Charlie | 70000 | 67500.00 | 1 |
| HR | Bob | 65000 | 67500.00 | 2 |
| Sales | Jack | 65000 | 62500.00 | 1 |
| Sales | Ivy | 60000 | 62500.00 | 2 |
它的优势:保留每行细节的同时完成分组级统计和排名,无需聚合“折叠”。
3、各数据库对窗口函数的支持与优化
| 特性 | MySQL 8.0+ |
PostgreSQL |
SQL Server |
Oracle |
|---|---|---|---|---|
| 窗口函数支持 | 完整 | 完整 | 完整 | 完整(最早) |
ROW_NUMBER() |
✔ | ✔ | ✔ | ✔ |
RANK(), DENSE_RANK() |
✔ | ✔ | ✔ | ✔ |
LEAD(), LAG() |
✔ | ✔ | ✔ | ✔ |
窗口帧(ROWS/RANGE) |
✔ | ✔ | ✔ | ✔ |
| 优化器处理 | 可下推至存储层 | 强(LATERAL) |
强(并行执行) | 极强(物化视图) |
我们要注意:MySQL 5.7及以下版本不支持窗口函数,需用变量或自连接模拟,性能差且容易出错。
八、CTE(Common Table Expressions)与递归查询的执行逻辑
CTE(公用表表达式)通过WITH子句定义临时结果集,提升SQL可读性和模块化能力。其执行逻辑因数据库而不同。
1、非递归CTE的执行顺序
CTE本身不改变整体执行顺序,但其物化(Materialization)行为影响性能。
1 | WITH high_performers AS ( |
执行流程:
(1)先执行CTE:high_performers被计算并存储为临时结果(可能物化)。
(2)主查询引用CTE:就像引用一张表一样,后续流程FROM → WHERE → GROUP BY → ...照常执行。
它的优点:逻辑清晰,避免深层嵌套子查询。
2、递归CTE的执行机制
递归CTE用于处理层次结构数据(如:组织架构、BOM物料清单),它的执行方式与普通查询完全不同。
1 | -- 示例:组织架构树(Oracle/PostgreSQL/SQL Server 支持) |
执行机制(迭代模型):
(1)执行锚点查询:获取根节点(如:CEO)。
(2)迭代执行递归部分:
- 第1轮:用锚点结果连接原表,找所有直接下属。
- 第2轮:用第1轮结果找下级,依此类推。
(3)直到无新行加入,停止迭代。
(4)合并所有轮次结果,去重(如果用UNION)。
(5)最后执行ORDER BY和LIMIT。
它的特点:非一次性扫描,而是多次迭代,适合树形结构但可能性能敏感。
3、各数据库CTE执行策略对比
| 数据库 | CTE是否默认物化 |
控制方式 | 递归支持 | 典型用途 |
|---|---|---|---|---|
MySQL 8.0+ |
否(可合并优化) | 无显式控制 | ✔ | 模块化查询 |
PostgreSQL |
默认物化 | MATERIALIZED/NOT MATERIALIZED |
✔ | 复杂ETL、递归树 |
SQL Server |
否(常转为视图) | 查询提示(如:OPTION) |
✔ | 报表分层计算 |
Oracle |
否(常内联) | MATERIALIZE/INLINE 提示 |
✔ | 层级查询、性能调优 |
我们的建议:
- 在PostgreSQL中,若CTE用于多次引用,可利用其物化特性避免重复计算。
- 在MySQL/SQL Server/Oracle中,CTE更像“语法糖”,优化器可能将其内联展开。
九、UNION与集合操作的执行顺序
UNION、UNION ALL、INTERSECT、EXCEPT等集合操作符用于合并多个查询结果,它们的执行顺序我们要特别注意。
1、执行顺序规则
(1)每个独立查询按标准顺序执行:FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY。
(2)集合操作合并结果:
UNION ALL:直接拼接,不去重。UNION:拼接后去重(隐含DISTINCT)。INTERSECT:取交集。EXCEPT:取差集。
(3)最终排序与限制:仅最外层可使用ORDER BY和LIMIT。
2、示例:
1 | (SELECT department, 'high' AS level FROM employees WHERE salary > 80000) |
执行流程:
(1)执行第一个SELECT:筛选高薪员工。
(2)执行第二个SELECT:筛选中等薪资员工。
(3)将两个结果集按行拼接(UNION ALL不去重)。
(4)对合并后的结果按department, level排序。
(5)返回最终结果。
我们要注意:
ORDER BY不能出现在子查询中(除非配合LIMIT/TOP)。- 各查询的
SELECT列数和类型必须兼容。 UNION自动去重,性能低于UNION ALL。
十、执行计划(Execution Plan)的解读与实践
尽管逻辑顺序统一,但数据库优化器会根据统计信息、索引、成本模型生成物理执行计划,因此,实际的物理执行顺序可能与逻辑顺序大相径庭。理解执行计划对我们调优来说很重要。
1、执行计划常见操作符
| 操作符 | 含义 | 性能提示 |
|---|---|---|
Seq Scan |
全表扫描 | 慢,应避免 |
Index Scan |
索引扫描 | 快,推荐 |
Index Only Scan |
覆盖索引扫描(无需回表) | 极快,理想状态 |
Hash Join |
哈希连接 | 大表连接常用 |
Merge Join |
归并连接 | 已排序数据高效 |
Nested Loop |
嵌套循环 | 小表驱动大表 |
Sort |
排序(filesort) |
慢,尽量用索引避免 |
HashAggregate |
哈希聚合 | 快,适合大数据 |
GroupAggregate |
分组聚合(需预排序) | 可利用有序索引 |
CTE Scan |
CTE物化后扫描 |
PostgreSQL常见 |
Subquery Scan |
子查询物化 | 可能性能瓶颈 |
2、我们建议
(1)我们优先使用EXPLAIN而非EXPLAIN ANALYZE:避免真实执行影响生产。
(2)我们要关注成本(cost)和行数(rows):预估是否合理。
(3)我们要检查是否使用索引:避免全表扫描。
(4)我们要识别性能热点:如Sort、Hash操作是否必要。
(5)我们要对比不同写法:如:IN vs EXISTS,JOIN vs 子查询。
十一:总结
我们掌握SQL,不仅是要会写查询,更要能理解它的理解执行顺序与实际(物理)执行顺序。
1、SQL执行顺序的完整逻辑模型
这是我们理解一切SQL行为的起点。因为所有性能优化、语法差异、调试技巧,都建立在这个逻辑顺序之上。它是我们的“SQL导航仪”。无论使用MySQL、PostgreSQL、Oracle还是SQL Server,以下逻辑顺序是通用的:
1 | 1、 FROM/JOIN/ON |
我们看说明:
- CTE(公用表表达式)在
FROM之前解析,但其内部查询仍遵循上述顺序。 - 递归CTE是独立执行模型,不适用线性顺序。
- 子查询在其所处位置按上述顺序独立执行,它们是“嵌套的完整查询”。
2、WHERE与HAVING:行过滤 vs 组过滤
WHERE→ 在分组前过滤原始行,作用于单行数据。HAVING→ 在分组后过滤聚合结果,作用于分组后的“组”。
我们的常见误区:用HAVING做本该WHERE完成的事 → 性能浪费。
正确、高效、不容易出错的写法:能用WHERE的,绝不拖到HAVING。
示例:HAVING salary > 5000(salary是原始列)WHERE salary > 5000 + HAVING AVG(salary) > 6000
3、GROUP BY的层级
GROUP BY A, B≠GROUP BY B, A→ 分组维度(组合)不同,结果集结构不同。- 分组后只能SELECT分组列或聚合函数 → 否则语法错误(标准SQL)或隐式转换(某些DB宽松模式)。
多条件分组讲层级:顺序决定分析粒度。
例如:GROUP BY 部门, 员工 → 每个员工一行;GROUP BY 员工, 部门 → 语义相同,但索引效率可能不同!
4、SELECT阶段,这是我们定义输出的地方
也是我们最容易误解的地方:
- 别名在
SELECT中定义 → 只能在后续ORDER BY、HAVING(部分DB)中使用。 - 窗口函数
OVER()在此阶段计算 → 不改变行数,只增加计算列。 - 聚合函数(如:
SUM,COUNT)在此阶段汇总 → 前提是已经GROUP BY。
我们要注意:SELECT是第5步,但我们在写SQL时它是第一个写的。这正是“声明式语言”的特点:我们先说“要什么”,数据库再决定“怎么做”。
5、DISTINCT与GROUP BY:去重的两种方法
DISTINCT→ 对最终结果集的所有列组合去重。GROUP BY→ 按指定列分组,可配合聚合函数,语义更明确。
优化建议:“SELECT DISTINCT user_id与SELECT user_id GROUP BY user_id在单列去重场景下等价,优化器会采用相同的索引策略;多列去重或需配合聚合时,优先使用GROUP BY,语义更清晰。
6、ORDER BY与LIMIT:最后的修饰与截取
ORDER BY可使用SELECT中定义的别名 → 因为它在SELECT之后执行。LIMIT/OFFSET是最后一步 → 但性能杀手常在此处!
7、我们的性能优化建议
(1)尽早过滤
我们要用WHERE减少数据量,避免无谓分组和排序。越早过滤,后续操作数据量越小。
(2)合理索引
WHERE列 → 单列或联合索引。ORDER BY列 → 覆盖索引避免filesort。GROUP BY列 → 联合索引匹配分组顺序。这里的顺序很重要!。
(3)我们要避免SELECT *
只选必要列 → 减少I/O、内存、网络传输,提升缓存效率。
(4)我们要慎用DISTINCT
我们优先用GROUP BY或业务逻辑去重 → 更高效、更可控。
(5)分页优化
- 避免
OFFSET大值 → 用游标分页(WHERE id > last_id LIMIT n)。 - MySQL中
LIMIT+ 索引排序可优化 → 但深度分页仍需游标。
(6)子查询 → JOIN
尤其在旧版MySQL中,我们需手动改写提升性能。现代优化器已改善,但我们仍建议要测试。
(7)利用CTE提高可读性
- PostgreSQL默认不合并CTE → 可能影响性能,可用
MATERIALIZED/NOT MATERIALIZED控制。 - 在复杂查询中,CTE是“逻辑分块”的神器,便于我们调试和协作。
8、跨数据库差异与调试技巧
- 语法差异:分页(
LIMITvsTOPvsFETCH)、别名使用范围、CTE行为等是跨库开发主要障碍。 - 优化器差异:相同SQL在不同库中执行计划可能天差地别 → 我们务必要用
EXPLAIN/EXPLAIN ANALYZE分析。 - 调试建议:用模拟数据分步验证 → 从
FROM开始,逐步添加WHERE、GROUP BY…观察中间结果。
9、从“写出SQL”到“写好SQL”
- 逻辑顺序是基石:
FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT - 语法差异需警惕:别让一个
TOP或CONNECT BY让我们加班。 - 优化器决定性能:写对是基础,写快是本事。我们要学会看执行计划。
- GROUP BY有顺序:维度与层级决定分析视角。
- WHERE和HAVING各司其职:一个管行,一个管组,分工明确。
- 实践出真知:没有比“分步调试+模拟数据”更适合我们的有效学习方式。



