SQL 里的 GROUP BY 和 HAVING,是数据库管理系统日常开发与数据分析岗位面试里最高频的两个子句,也是从“会查表”走向“会统计”的分水岭。很多人能背出语法,但一遇到“用 WHERE 还是 HAVING”“为什么 HAVING 不能单独使用”“GROUP BY 之后能不能 SELECT 其他列”这类问题时就卡壳。本文将用一份结构化员工表作为测试数据,从语法、执行顺序、聚合函数配合、性能优化到常见报错排查,完整拆解 GROUP BY 和 HAVING 的用法。
读者看完本文,能够直接在自己的 MySQL、PostgreSQL、SQL Server 或 SQLite 环境中验证所有示例,并能回答绝大多数数据库管理系统相关的 SQL 分组统计面试题。先看核心能力速览,再逐步展开语法细节和实操验证。
1. GROUP BY 与 HAVING 核心能力速览
| 能力项 | 说明 |
|---|---|
| 所属概念 | SQL 查询语句中的分组子句与分组过滤子句 |
| 常用数据库 | MySQL、PostgreSQL、SQL Server、Oracle、SQLite、DM 等 |
| GROUP BY 作用 | 按一个或多个列对查询结果分组,配合聚合函数做统计 |
| HAVING 作用 | 对 GROUP BY 生成的分组结果进行条件过滤 |
| 与 WHERE 区别 | WHERE 在分组前过滤行,HAVING 在分组后过滤分组 |
| 是否必须搭配 GROUP BY | HAVING 通常与 GROUP BY 一起使用;单独使用场景较少 |
| 典型聚合函数 | COUNT、SUM、AVG、MAX、MIN、GROUP_CONCAT(方言函数) |
| 执行顺序 | FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT |
| 适用场景 | 分组统计、报表分析、数据清洗、面试笔试、业务看板 |
| 常见报错 | “not functionally dependent on columns in GROUP BY clause” |
| 性能要点 | 分组列建议建立索引;尽量避免对无索引列做大分组统计 |
从能力速览可以看出,GROUP BY 解决的是“如何分组统计”,HAVING 解决的是“如何过滤统计结果”,两者组合后才构成完整的聚合查询能力。
2. 适用场景与使用边界
2.1 适合什么场景
GROUP BY 和 HAVING 最典型的场景是数据统计。
业务开发中,下面几类需求几乎每天都要写:
- 统计每个部门的员工人数:按
dept_name分组,COUNT(*)统计人数。 - 统计每个部门的平均薪资:按
dept_name分组,AVG(salary)计算平均值。 - 筛选平均薪资超过阈值的部门:先用
GROUP BY dept_name分组,再用HAVING AVG(salary) > 10000过滤。 - 统计每月入职人数:按
DATE_FORMAT(hire_date, '%Y-%m')分组,再COUNT(*)。 - 统计订单表中每个客户的累计消费金额:按
customer_id分组,SUM(amount)求总额。 - 找出重复数据:按业务主键分组,
HAVING COUNT(*) > 1定位重复记录。
数据分析师、后端开发、运维人员在写报表 SQL 和接口查询时,都会高频用到这两个子句。
2.2 不建议用在什么场景
GROUP BY 不是万能的。以下场景需要谨慎:
- 数据量特别大且无分组索引时,GROUP BY 会触发全表扫描和临时表排序,性能可能很差。
- 业务需要保留分组内明细数据时,
GROUP BY只能输出分组列和聚合结果,明细行被折叠;此时更合适的是窗口函数ROW_NUMBER()、PARTITION BY或子查询。 - 需要跨分组计算占比、同比环比时,单独使用 GROUP BY 不够灵活,建议配合窗口函数
SUM(...) OVER (PARTITION BY ...)。 - 字符串拼接、按组去重等需求存在数据库方言差异,例如 MySQL 的
GROUP_CONCAT、PostgreSQL 的STRING_AGG、SQL Server 的FOR XML PATH,迁移时要注意。
2.3 使用边界与规范提醒
在真实数据库管理系统中操作数据时,有一点必须强调:如果分组统计涉及用户实名信息、手机号、订单金额等敏感数据,测试环境要使用脱敏数据,不能把生产库的原始敏感记录直接导出到个人电脑。SQL 本身是通用技术,但数据访问权限、隐私保护和合规边界需要由执行者自行把控。
3. SQL 执行环境准备
本文示例以 MySQL 语法为主,同时说明 PostgreSQL 和 SQL Server 的差异。读者不需要专门安装完整数据库,也可以使用 SQLite 内存库、在线 SQL 练习平台或本地 Docker 容器验证。
推荐几种准备方式:
# 方式一:Docker 快速启动 MySQL 8 docker run --name mysql-test -e MYSQL_ROOT_PASSWORD=123456 -p 3306:3306 -d mysql:8# 方式二:Docker 快速启动 PostgreSQL 16 docker run --name pg-test -e POSTGRES_PASSWORD=123456 -p 5432:5432 -d postgres:16如果本机已经有 MySQL 或 SQL Server 实例,直接使用现有实例即可。创建测试库和测试表:
CREATE DATABASE IF NOT EXISTS sql_demo DEFAULT CHARACTER SET utf8mb4; USE sql_demo; CREATE TABLE employees ( emp_id INT PRIMARY KEY AUTO_INCREMENT, emp_name VARCHAR(50) NOT NULL, dept_name VARCHAR(50) NOT NULL, salary DECIMAL(10, 2) NOT NULL, hire_date DATE NOT NULL );插入测试数据:
INSERT INTO employees (emp_name, dept_name, salary, hire_date) VALUES ('张三', '技术部', 15000.00, '2021-03-15'), ('李四', '技术部', 18000.00, '2020-07-01'), ('王五', '产品部', 12000.00, '2022-01-10'), ('赵六', '产品部', 11000.00, '2021-11-20'), ('孙七', '产品部', 16000.00, '2019-05-06'), ('周八', '运营部', 9000.00, '2023-02-14'), ('吴九', '运营部', 9500.00, '2022-08-30'), ('郑十', '技术部', 22000.00, '2018-09-12'), ('钱十一', '运营部', 8500.00, '2024-01-05');后续所有示例都基于这张表。表中包含 4 个部门:技术部 3 人、产品部 3 人、运营部 3 人,薪资覆盖 8500 到 22000。
4. GROUP BY 子句基础用法
4.1 基本语法
GROUP BY 子句用于将查询结果按照一个或多个列进行分组。
SELECT 分组列, 聚合函数(统计列) FROM 表名 WHERE 行级过滤条件 GROUP BY 分组列;核心逻辑是:数据库先把满足 WHERE 条件的行取出来,然后按照 GROUP BY 指定的列把相同的值归为一组,最后对每组执行聚合函数计算。
4.2 单列分组
统计每个部门的员工人数:
SELECT dept_name, COUNT(*) AS emp_count FROM employees GROUP BY dept_name;执行结果:
| dept_name | emp_count |
|---|---|
| 技术部 | 3 |
| 产品部 | 3 |
| 运营部 | 3 |
SELECT dept_name, SUM(salary) AS total_salary, AVG(salary) AS avg_salary, MAX(salary) AS max_salary, MIN(salary) AS min_salary FROM employees GROUP BY dept_name;执行结果:
| dept_name | total_salary | avg_salary | max_salary | min_salary |
|---|---|---|---|---|
| 技术部 | 55000.00 | 18333.3333 | 22000.00 | 15000.00 |
| 产品部 | 39000.00 | 13000.0000 | 16000.00 | 11000.00 |
| 运营部 | 27000.00 | 9000.0000 | 9500.00 | 8500.00 |
从结果可以看到,GROUP BY 把原始 9 行数据折叠成了 3 个分组,每个分组输出一行统计结果。这就是“分组聚合”的本质。
4.3 多列分组
实际业务经常需要按多个维度分组,例如“按部门和年份统计人数”。
SELECT dept_name, YEAR(hire_date) AS hire_year, COUNT(*) AS emp_count FROM employees GROUP BY dept_name, YEAR(hire_date) ORDER BY dept_name, hire_year;多列分组的规则是:先按第一列分组,再按第二列分组,只有组合值完全相同的行才归入同一组。这种写法在订单报表、用户分群统计中非常常见。
4.4 GROUP BY 的 SELECT 列限制
这里要重点说明一个坑。在标准 SQL 和 MySQL 8 默认配置下,SELECT 中出现的非聚合列必须出现在 GROUP BY 子句中。
-- 错误示例:emp_name 不在 GROUP BY 中 SELECT dept_name, emp_name, COUNT(*) FROM employees GROUP BY dept_name;这条语句如果直接执行可能报错,或者在旧 MySQL 版本中会随机返回某个员工的姓名。原因是一个分组里有多个 emp_name,数据库不知道该返回哪一个。
正确做法是只选择分组列和聚合函数:
SELECT dept_name, COUNT(*) FROM employees GROUP BY dept_name;PostgreSQL 和 SQL Server 在这点上非常严格,只要 SELECT 中出现非分组非聚合列,直接报错。建议所有环境下都遵循“SELECT 的列要么在 GROUP BY 里,要么被聚合函数包裹”的规范。
5. HAVING 子句用法
5.1 HAVING 解决的问题
WHERE 子句无法过滤聚合函数的结果。例如需求是“找出平均薪资超过 10000 的部门”,写成下面这样会报错:
-- 错误示例:WHERE 中不能直接使用聚合函数 SELECT dept_name, AVG(salary) AS avg_salary FROM employees WHERE AVG(salary) > 10000 GROUP BY dept_name;WHERE 是逐行过滤,过滤发生在分组之前,此时聚合函数还没有计算结果,因此不允许使用AVG(salary)这种聚合条件。正确写法是使用 HAVING:
SELECT dept_name, AVG(salary) AS avg_salary FROM employees GROUP BY dept_name HAVING AVG(salary) > 10000;执行结果:
| dept_name | avg_salary |
|---|---|
| 技术部 | 18333.3333 |
| 产品部 | 13000.0000 |
运营部平均薪资 9000,被过滤掉了。
5.2 HAVING 单独使用
HAVING 不强制要求前面必须有 GROUP BY。在 MySQL 中,可以写:
SELECT COUNT(*) AS total_emp FROM employees HAVING COUNT(*) > 5;此时整张表被当作一个大分组,COUNT(*) 返回 9,条件成立。但这种写法可读性较差,等价于WHERE COUNT(*) > 5的语义,而标准 SQL 中更推荐使用 WHERE 或子查询。日常开发建议始终让 HAVING 与 GROUP BY 配合出现,语义更清晰。
5.3 HAVING 中可以使用多个聚合条件
HAVING 支持多个条件的组合,也可以使用别名。例如筛选员工数大于 2 人、平均薪资大于 8000 的部门:
SELECT dept_name, COUNT(*) AS emp_count, AVG(salary) AS avg_salary FROM employees GROUP BY dept_name HAVING COUNT(*) > 2 AND AVG(salary) > 8000 ORDER BY avg_salary DESC;执行结果:
| dept_name | emp_count | avg_salary |
|---|---|---|
| 技术部 | 3 | 18333.3333 |
| 产品部 | 3 | 13000.0000 |
| 运营部 | 3 | 9000.0000 |
HAVING 中也可以使用 SELECT 中的别名:
SELECT dept_name, AVG(salary) AS avg_salary FROM employees GROUP BY dept_name HAVING avg_salary > 10000;MySQL 支持在 HAVING 中引用别名,但 PostgreSQL 和 SQL Server 对别名的解析顺序不同,部分场景可能需要重复写聚合表达式。为了可移植性,建议直接写完整聚合表达式。
6. WHERE 与 HAVING 的区别
这部分是面试重点,也是日常开发最容易混淆的地方。
| 对比维度 | WHERE | HAVING |
|---|---|---|
| 过滤时机 | 分组之前过滤行 | 分组之后过滤分组 |
| 能否使用聚合函数 | 不能 | 可以 |
| 是否必须搭配 GROUP BY | 否,可独立使用 | 通常搭配 GROUP BY |
| 性能影响 | 先缩小数据量,通常更高效 | 分组计算后再过滤,数据量大时开销高 |
| 与 SELECT 别名关系 | 通常不能使用 SELECT 别名 | 部分数据库支持 |
| 执行顺序 | 先执行 | 后执行 |
用一个组合查询演示两者同时出现:
需求:统计 2022 年之前入职的员工中,每个部门平均薪资大于 12000 的部门。
SELECT dept_name, AVG(salary) AS avg_salary FROM employees WHERE hire_date < '2022-01-01' GROUP BY dept_name HAVING AVG(salary) > 12000;先由 WHERE 过滤掉 2022 年之后入职的员工,再由 GROUP BY 分组,最后用 HAVING 筛掉平均薪资不达标的部门。
需要注意,WHERE 和 HAVING 的执行顺序决定了同一个条件写在两个位置效果可能不同。例如“统计部门人数大于 1”的部门,若在 WHERE 中写COUNT(*) > 1会直接报错;若在 WHERE 中写普通行过滤条件,则会先减少参与分组的行数,影响最终分组结果。
所以结论是:行级条件放 WHERE,分组级条件放 HAVING。
7. SQL 子句执行顺序
理解执行顺序是写出正确 SQL 的关键。一个完整查询的标准执行顺序为:
FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT逐层拆解:
| 序号 | 子句 | 作用 |
|---|---|---|
| 1 | FROM | 确定数据来源表 |
| 2 | WHERE | 过滤原始行,剔除不满足条件的记录 |
| 3 | GROUP BY | 对过滤后的行进行分组 |
| 4 | HAVING | 过滤分组结果 |
| 5 | SELECT | 计算选择列、聚合函数、表达式 |
| 6 | ORDER BY | 对最终结果排序 |
| 7 | LIMIT | 限制返回行数 |
这个顺序解释了三个常见问题:
- 为什么 WHERE 不能使用聚合函数?因为执行到 WHERE 时还没分组。
- 为什么 HAVING 可以使用聚合函数?因为执行到 HAVING 时已经完成分组和聚合计算。
- 为什么 ORDER BY 可以使用别名?因为执行到 ORDER BY 时 SELECT 已经计算完成。
在 MySQL 中,GROUP BY 隐式使用顺序排序;如果不需要排序,可以在 GROUP BY 后面加ORDER BY NULL来避免额外排序开销(MySQL 5.7 之前的版本有优化价值,MySQL 8 基本不再需要)。PostgreSQL 不会对 GROUP BY 结果隐式排序,需要排序时显式加 ORDER BY。
8. 高级分组统计场景
8.1 分组后按聚合结果排序
统计每个部门的平均薪资并按平均薪资降序排列:
SELECT dept_name, AVG(salary) AS avg_salary FROM employees GROUP BY dept_name ORDER BY avg_salary DESC;注意这里 ORDER BY 中使用的是 SELECT 别名avg_salary,在 MySQL 中可以直接使用。如果 ORDER BY 中使用聚合函数,写法为:
ORDER BY AVG(salary) DESC;8.2 分组后限制返回数量
按部门分组统计总薪资,取薪资最高的两个部门:
SELECT dept_name, SUM(salary) AS total_salary FROM employees GROUP BY dept_name ORDER BY total_salary DESC LIMIT 2;这条语句在 MySQL、PostgreSQL、SQLite 中都支持。SQL Server 不支持LIMIT,需要改写为SELECT TOP 2 ... ORDER BY total_salary DESC。
8.3 分组后查找重复数据
数据清洗场景非常常用。假设员工表里 emp_name 允许重复,要找出重名员工:
SELECT emp_name, COUNT(*) AS cnt FROM employees GROUP BY emp_name HAVING COUNT(*) > 1;因为当前测试数据没有重名,所以结果为空。如果有一张订单表想找相同订单号重复记录,直接换成GROUP BY order_no HAVING COUNT(*) > 1即可。
8.4 按时间维度分组统计
按月统计入职人数:
SELECT DATE_FORMAT(hire_date, '%Y-%m') AS hire_month, COUNT(*) AS emp_count FROM employees GROUP BY DATE_FORMAT(hire_date, '%Y-%m') ORDER BY hire_month;PostgreSQL 写法为TO_CHAR(hire_date, 'YYYY-MM'),SQL Server 写法为FORMAT(hire_date, 'yyyy-MM')。时间分组的方言差异在跨数据库迁移时需要单独处理。
8.5 条件聚合
在 GROUP BY 内部用CASE WHEN做条件聚合,可以一次查出多个指标:
SELECT dept_name, COUNT(*) AS total_count, SUM(CASE WHEN salary >= 12000 THEN 1 ELSE 0 END) AS high_salary_count FROM employees GROUP BY dept_name;执行结果:
| dept_name | total_count | high_salary_count |
|---|---|---|
| 技术部 | 3 | 3 |
| 产品部 | 3 | 2 |
| 运营部 | 3 | 0 |
这种方式在报表 SQL 中非常实用,一次遍历即可完成多条件统计,效率比多次子查询更高。
8.6 结合窗口函数做组内排名
如果希望在分组统计的同时保留每条记录的组内排名,可以使用窗口函数:
SELECT emp_name, dept_name, salary, ROW_NUMBER() OVER (PARTITION BY dept_name ORDER BY salary DESC) AS rank_in_dept FROM employees;这里PARTITION BY和GROUP BY不同,它不折叠行,而是保持明细数据不变,同时计算组内排名。当 GROUP BY 无法满足“既要明细又要组内统计”的需求时,窗口函数是更好的选择。
9. 性能优化建议
9.1 索引与 GROUP BY
GROUP BY 本质上分两步:先扫描数据,再对分组列排序或使用哈希分组。MySQL 8 使用哈希分组后性能优于早期版本,但如果分组列没有索引,数据量增大后依然可能产生临时文件和磁盘排序。
一个通用的优化原则是:为 GROUP BY 的分组列建立联合索引。
CREATE INDEX idx_emp_dept ON employees(dept_name, hire_date);如果查询经常按部门和入职时间分组,这个联合索引可以直接覆盖分组列,减少排序开销。如果还要带 WHERE 条件,例如WHERE hire_date >= '2022-01-01' GROUP BY dept_name,索引设计需要遵循最左前缀原则。
9.2 WHERE 前移
先通过 WHERE 缩小参与分组的数据集,再分组,性能最好。例如下面两条语句在数据量大时性能差异明显:
-- 推荐:先过滤再分组 SELECT dept_name, AVG(salary) FROM employees WHERE hire_date >= '2022-01-01' GROUP BY dept_name;-- 不推荐:分组后再过滤,临时表数据量更大 SELECT dept_name, AVG(salary) FROM employees GROUP BY dept_name HAVING MIN(hire_date) >= '2022-01-01';二者的语义并不完全等价,但表达了一个核心思路:能用 WHERE 提前过滤的行,不要留到 HAVING 阶段处理。
9.3 避免不必要的分组列
GROUP BY 后面每多一列,分组数可能成倍增加,内存和临时表开销也会增大。写报表 SQL 前先确认是否真的需要多维分组。如果只需要总记录数,直接SELECT COUNT(*) FROM table比GROUP BY分组再数更快。
9.4 使用 EXPLAIN 观察执行计划
MySQL 中使用EXPLAIN查看 GROUP BY 是否触发临时表和文件排序:
EXPLAIN SELECT dept_name, COUNT(*) FROM employees GROUP BY dept_name;重点观察 Extra 列是否出现Using temporary和Using filesort。如果出现,说明当前分组列的索引利用不充分,需要考虑调整索引或改写查询。
9.5 分批处理避免大分组
超大表的聚合统计一次性执行容易造成长事务和锁竞争。实际工程中建议按时间范围分批统计,再把结果合并。例如按年循环统计月订单量,然后插入汇总表。这种方式牺牲了实时性,但稳定性更高。
10. 常见问题与排查方法
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 报错“not functionally dependent on columns in GROUP BY clause” | SELECT 中包含了非分组列和非聚合列 | 检查 SELECT 列表 | 将非分组列加入 GROUP BY,或包裹在聚合函数中 |
| WHERE 中使用聚合函数报错 | SQL 语法不允许在 WHERE 中使用聚合函数 | 查看错误语句位置 | 把聚合条件移到 HAVING |
| HAVING 中使用 SELECT 别名在某些数据库不生效 | 不同数据库对 SELECT 别名解析顺序不同 | 查看数据库版本文档 | 在 HAVING 中写完整聚合表达式 |
| GROUP BY 结果顺序不稳定 | MySQL 8 不再对 GROUP BY 隐式排序 | 多次执行对比结果 | 显式添加 ORDER BY |
| 分组后想要的明细数据丢失 | GROUP BY 会折叠行 | 检查业务需求 | 改用窗口函数或子查询 |
| GROUP BY 查询特别慢 | 分组列无索引,或未先 WHERE 过滤 | 执行 EXPLAIN 查看执行计划 | 为分组列建索引,WHERE 条件前置 |
| COUNT(1) 和 COUNT(*) 结果不一致 | 表中存在 NULL 值列 | 对比不同计数列 | 明确统计目标;COUNT(列名) 不包含 NULL 行 |
| 使用 GROUP_CONCAT 时字符串过长被截断 | group_concat_max_len 参数限制 | 查看 MySQL 参数 | 执行 SET SESSION group_concat_max_len = 100000 |
下面重点拆解两个高频报错。
第一个高频报错是“is not functionally dependent on columns in GROUP BY clause”。这个问题在 MySQL 8、PostgreSQL 和 SQL Server 中都会出现。原因就是 SELECT 列表里出现了既不是分组列、也没有被聚合函数包裹的列。解决方式只有两种:把该列加入 GROUP BY,或者用ANY_VALUE()、MAX()、MIN()等函数包起来。不过更推荐思考业务是否真的需要取这个列。
第二个高频问题是“WHERE 和 HAVING 用混”。典型表现是过滤聚合结果时习惯性写在 WHERE,然后报语法错误。记住执行顺序即可:WHERE 是先过滤原始行,HAVING 是后过滤分组。如果条件里出现了COUNT、SUM、AVG、MAX、MIN,这个条件就只能放 HAVING。
11. 最佳实践
11.1 分组列后置,放 SELECT 最右侧
阅读和调试时,建议先看分组列是什么,再看聚合指标是什么。保持 SELECT 中第一列是分组列,后面依次是聚合列,可读性最好:
SELECT dept_name, COUNT(*) AS emp_count, ROUND(AVG(salary), 2) AS avg_salary FROM employees GROUP BY dept_name;11.2 注意 NULL 分组
GROUP BY 会把 NULL 值单独作为一组。如果分组列存在 NULL,统计结果中会出现一个NULL分组。需要过滤时可以用HAVING dept_name IS NOT NULL或提前在 WHERE 中过滤。
11.3 聚合结果重命名
聚合函数返回的列名通常很长且不直观。使用别名AS avg_salary可以方便后续排序和程序解析结果集。别名在 ORDER BY 中可以使用,在 HAVING 中建议谨慎使用完整表达式。
11.4 多环境验证
不同数据库管理系统对 GROUP BY 和 HAVING 的语法兼容性有差异。同样的语句在 MySQL 能跑,在 PostgreSQL 或 SQL Server 不一定能跑。团队协作时建议约定统一使用标准 SQL 写法,避免依赖某个数据库的宽松语法。例如不要依赖 MySQL 对非聚合列的宽松处理,尽量做到 SELECT 列全部规范化。
11.5 输出目录与脚本管理
如果经常写分组统计 SQL,建议把常用统计脚本做成数据库视图或存储过程,避免重复粘贴长 SQL。例如可以创建一个部门统计视图:
CREATE VIEW v_dept_stats AS SELECT dept_name, COUNT(*) AS emp_count, AVG(salary) AS avg_salary FROM employees GROUP BY dept_name;后续查询直接SELECT * FROM v_dept_stats WHERE avg_salary > 10000;,开发效率更高,也便于统一口径。
12. 总结
GROUP BY 和 HAVING 是 SQL 聚合查询的基石。GROUP BY 负责把数据按维度折叠成组,HAVING 负责对分组结果做二次过滤,二者配合聚合函数来完成绝大多数统计需求。理解执行顺序是掌握这两个子句的关键:FROM 先取数,WHERE 先过滤行,GROUP BY 完成分组,HAVING 过滤分组,最后 SELECT 输出结果。写 SQL 时记住一个原则:行级条件放 WHERE,分组条件放 HAVING,SELECT 中要么是分组列要么是聚合列,就不会再出现语法混淆。
建议读者把本文的表结构和 12 条示例 SQL 全部在自己的数据库管理系统中跑一遍,重点关注三点:单列分组与多列分组的区别、WHERE 与 HAVING 的执行顺序差异、以及 GROUP BY 对 SELECT 列的限制。跑通之后再尝试优化部门统计查询的索引,用 EXPLAIN 观察执行计划变化。本文的示例可以直接作为面试前的 SQL 复习资料,收藏备用即可。