搞懂这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、FROM/JOIN
2、WHERE
3、GROUP BY
4、HAVING
5、SELECT
6、DISTINCT
7、ORDER BY
8、LIMIT/OFFSET/TOP/FETCH
我们要注意:这是“逻辑执行顺序”,不是“实际(物理)执行顺序”。真实执行时,数据库优化器可能根据索引、统计信息、成本估算等调整物理执行路径,但最终结果可认为等价于按此逻辑执行顺序执行。
下面我们对每个步骤进行详细说明:
1、FROM/JOIN,确定数据源
这是我们SQL查询的第一步。数据库引擎从FROM子句指定的表或视图中读取数据。如果存在JOIN,则在此阶段完成表连接操作(如:INNER JOIN, LEFT JOIN等等)。
- 作用:构建初始数据集(虚拟表)。
- 可访问内容:表中的所有列。
- 不可访问内容:SELECT中定义的别名、聚合函数结果。
2、WHERE,行级过滤
对FROM阶段生成的数据集进行条件过滤,仅保留满足WHERE条件的行。
- 作用:减少后续处理的数据量,提高效率。
- 可访问内容:原始表列、连接后的列。
- 不可访问内容:
- 聚合函数(如:COUNT(), SUM())→ 因为尚未分组。
- SELECT中定义的列别名 → 因为SELECT尚未执行。
正确写法:
WHERE salary > 50000
错误写法:
WHERE COUNT(*) > 5 -- 聚合函数不能在WHERE中使用
WHERE avg_salary > 70000 -- 别名尚未定义
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),但非标准。
正确写法:
HAVING COUNT(*) > 2
HAVING AVG(salary) > 70000
错误写法(在严格数据库中):
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、SQL Server、Oracle为例进行对比。
1、MySQL特性差异:
- 别名支持灵活: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行,极大提升性能。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显式控制。WITH cte AS (SELECT * FROM large_table WHERE condition)
SELECT * FROM cte JOIN other_table ... -- CTE 独立执行 - DISTINCT ON:独有语法,按指定列去重并保留“第一行”(需配合ORDER BY)。SELECT DISTINCT ON (department) name, salary, department
FROM employees
ORDER BY department, salary DESC; -- 每个部门工资最高者 - HAVING严格:不支持在HAVING中使用SELECT别名,我们必须写完整表达式。HAVING COUNT(*) > 5 -- 必须写完整表达式
HAVING emp_count > 5 -- 不支持(除非emp_count是分组列)
3、SQL Server特性差异:
- 分页语法:使用标准OFFSET ... FETCH(2012+),旧版用TOP。SELECT * FROM t
ORDER BY id
OFFSET 10 ROWS FETCH NEXT 20 ROWS ONLY; - APPLY运算符:支持CROSS APPLY/OUTER APPLY,用于关联表值函数或子查询。SELECT 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,结果顺序不确定。SELECT 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作用于排序后的结果集(避免分页混乱)。-- 错误:先分配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。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
CREATE TABLE employees (
id INT,
name VARCHAR(50),
department VARCHAR(50),
gender CHAR(1), -- 'M' or 'F'
salary DECIMAL(10,2),
hire_year INT
);
INSERT INTO employees VALUES
(1, 'Alice', 'HR', 'F', 60000, 2019),
(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),
(8, 'Henry', 'Sales', 'M', 55000, 2019),
(9, 'Ivy', 'Sales', 'F', 60000, 2021),
(10, 'Jack', 'Sales', 'M', 65000, 2022);
2、查询示例:多条件分组+过滤+排序+分页
SELECT
department,
gender,
COUNT(*) AS emp_count,
AVG(salary) AS avg_salary
FROM employees
WHERE hire_year >= 2020
GROUP BY department, gender
HAVING COUNT(*) >= 2
ORDER BY department, avg_salary DESC
LIMIT 2;
3、分步执行过程(逻辑顺序)
步骤1:FROM+WHERE,过滤原始行
-- 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)
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,过滤分组
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,定义输出列和别名
SELECT department, gender, COUNT(*) AS emp_count, AVG(salary) AS avg_salary
输出结构确定,别名emp_count和avg_salary生效。
步骤5:ORDER BY,排序
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行
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 → 分组后过滤组 → 无法减少分组计算量 → 相对低效
正确用法:
-- 查询平均工资 > 70000的部门
SELECT department, AVG(salary) as avg_sal
FROM employees
WHERE salary > 50000 -- 先过滤低薪员工(减少分组数据)
GROUP BY department
HAVING AVG(salary) > 70000; -- 再过滤平均工资低的部门
五、多条件GROUP BY执行详解
当GROUP BY包含多个列时,分组是层级嵌套的,顺序至关重要。
1、示例:GROUP BY A, B与GROUP BY B, A的区别
查询1:GROUP BY department, gender
SELECT department, gender, COUNT(*)
FROM employees
WHERE hire_year >= 2020
GROUP BY department, gender;
结果:
department | gender | COUNT |
HR | M | 2 |
Engineering | M | 2 |
Engineering | F | 2 |
Sales | F | 1 |
Sales | M | 1 |
→ 先按部门分,部门内再按性别分。
查询2:GROUP BY gender, department
SELECT gender, department, COUNT(*)
FROM employees
WHERE hire_year >= 2020
GROUP BY gender, department;
结果:
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、示例:关联子查询
SELECT name, salary, department
FROM employees e
WHERE salary > (
SELECT AVG(salary)
FROM employees
WHERE department = e.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之前完成。
...
5、SELECT
→ 普通列、表达式、聚合函数
→ 窗口函数计算(此时结果集已确定,但尚未去重或排序)
6、DISTINCT
7、ORDER BY
8、LIMIT
我们看关键点:窗口函数可以引用SELECT中定义的别名,但不能在WHERE、GROUP BY或HAVING中使用,因为这些阶段早于SELECT。
2、示例:窗口函数在分组后计算排名
SELECT
department,
name,
salary,
AVG(salary) OVER (PARTITION BY department) AS dept_avg,
RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS rank_in_dept
FROM employees
WHERE hire_year >= 2020
ORDER BY department, rank_in_dept;
执行流程解析:
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)行为影响性能。
WITH high_performers AS (
SELECT * FROM employees
WHERE salary > 80000
)
SELECT department, COUNT(*)
FROM high_performers
GROUP BY department;
执行流程:
(1)先执行CTE:high_performers被计算并存储为临时结果(可能物化)。
(2)主查询引用CTE:就像引用一张表一样,后续流程FROM → WHERE → GROUP BY → ...照常执行。
它的优点:逻辑清晰,避免深层嵌套子查询。
2、递归CTE的执行机制
递归CTE用于处理层次结构数据(如:组织架构、BOM物料清单),它的执行方式与普通查询完全不同。
-- 示例:组织架构树(Oracle/PostgreSQL/SQL Server 支持)
WITH RECURSIVE org_tree AS (
-- 锚点查询(初始行)
SELECT id, name, manager_id, 1 AS level
FROM employees
WHERE manager_id IS NULL
UNION ALL
-- 递归查询
SELECT e.id, e.name, e.manager_id, ot.level + 1
FROM employees e
JOIN org_tree ot ON e.manager_id = ot.id
)
SELECT * FROM org_tree ORDER BY level, name;
执行机制(迭代模型):
(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、示例:
(SELECT department, 'high' AS level FROM employees WHERE salary > 80000)
UNION ALL
(SELECT department, 'mid' AS level FROM employees WHERE salary BETWEEN 60000 AND 80000)
ORDER BY department, level;
执行流程:
(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、 FROM/JOIN/ON
2、 WHERE
3、 GROUP BY
4、 HAVING
5、 SELECT
→ 普通列、表达式
→ 聚合函数
→ 窗口函数(OVER)
→ 列别名定义
6、 DISTINCT
7、 UNION/INTERSECT/EXCEPT(集合操作)
8、 ORDER BY(可使用SELECT别名)
9、 LIMIT/OFFSET/FETCH/TOP
我们看说明:
- 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、跨数据库差异与调试技巧
- 语法差异:分页(LIMIT vs TOP vs FETCH)、别名使用范围、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各司其职:一个管行,一个管组,分工明确。
- 实践出真知:没有比“分步调试+模拟数据”更适合我们的有效学习方式。