4. 执行计划基础:操作符、访问路径、连接方法、计划树
执行计划这东西,说白了就是数据库告诉你它打算怎么干活的说明书。我见过不少开发同事,SQL写得飞起,但一遇到慢查询就抓瞎——其实就是看不懂执行计划。今天咱们把这层窗户纸捅破。
4.1 操作符:执行计划的最小单元
每个执行计划都由操作符组成。操作符就是数据库执行SQL时的基本动作单元。我个人习惯把操作符分成三类:
- 数据访问操作符:从磁盘或内存拿数据
- 数据连接操作符:把两个数据集拼起来
- 数据计算操作符:排序、聚合、过滤等
举个例子,一个简单的查询:
SELECT * FROM orders WHERE status = 'SHIPPED';
它的执行计划可能包含两个操作符:
TABLE ACCESS FULL— 全表扫描FILTER— 过滤条件
嗯,这里要注意:操作符是自底向上执行的。最底层的操作符先干活,结果往上传递。
核心要点:操作符的排列顺序就是执行顺序。看懂操作符,你就看懂了数据库的"施工图纸"。
4.2 访问路径:数据库怎么找到数据
访问路径,就是数据库获取数据的方式。我在项目中遇到过最典型的场景:同样的SQL,换个访问路径,性能差了几十倍。
常见的访问路径有这些:
| 访问路径 | 说明 | 适用场景 |
|---|---|---|
| 全表扫描 | 从头到尾读整张表 | 小表、返回数据量大 |
| 索引唯一扫描 | 通过唯一索引定位一行 | 等值查询,有唯一索引 |
| 索引范围扫描 | 通过索引查找一个范围 | 范围查询、排序 |
| 索引全扫描 | 扫描整个索引结构 | 覆盖索引、排序 |
| 索引快速全扫描 | 多块读方式扫描索引 | 聚合查询、大表统计 |
避坑指南:我曾经接手过一个慢查询,全表扫描要跑45秒。加了个合适的索引后,变成索引范围扫描,0.3秒就出结果了。你想想看,选对访问路径有多重要。
4.3 连接方法:表与表怎么拼
多表查询时,数据库需要决定怎么把表连接起来。常用的连接方法有三种:
嵌套循环连接(Nested Loops Join)
外层表每读一行,就去内层表找匹配行。适合小表驱动大表,或者内层表有高效索引的场景。
-- 伪代码逻辑
FOR outer_row IN outer_table LOOP
FOR inner_row IN inner_table
WHERE inner_row.key = outer_row.key LOOP
-- 返回匹配行
END LOOP;
END LOOP;
哈希连接(Hash Join)
把一张表建成哈希表,另一张表逐行去探测。适合大表等值连接,没有索引也能跑得不错。
排序合并连接(Sort Merge Join)
两张表各自排序,然后像拉链一样合并。适合排序好的数据,或者非等值连接。
| 连接方法 | 内存消耗 | 适用场景 | 典型问题 |
|---|---|---|---|
| 嵌套循环 | 低 | 小表驱动大表 | 驱动表选错会慢 |
| 哈希连接 | 高 | 大表等值连接 | 内存不够会溢出 |
| 排序合并 | 中 | 非等值、已排序 | 排序开销大 |
注意:连接方法选错了,性能可能差几个数量级。我见过一个案例,嵌套循环连接跑了2小时,改成哈希连接后2分钟搞定。所以别小看这个选择。
4.4 计划树:把一切串起来
操作符、访问路径、连接方法,这些不是孤立的。它们组合起来,就形成了一棵计划树。
计划树的特点:
- 树形结构:叶子节点是数据访问,根节点是最终结果
- 自底向上执行:子节点先执行,父节点后执行
- 每个节点都有成本:数据库优化器会估算每个节点的代价
来看一个实际例子:
SELECT o.order_id, c.name
FROM orders o
JOIN customers c ON o.customer_id = c.id
WHERE o.status = 'SHIPPED'
ORDER BY o.order_date;
这个SQL的计划树可能是这样的:
看懂这棵树,你就能回答这些问题:
- 数据库先访问哪张表?——看叶子节点,先读orders再读customers
- 用了什么连接方法?——哈希连接
- 排序发生在什么时候?——连接之后,输出之前
我的习惯:拿到一个慢查询,我第一件事就是看计划树。从根节点往下看,找到成本最高的那个节点,那就是性能瓶颈所在。说白了,优化就是找到这棵树上最粗的那根树枝,把它砍细。
4.5 怎么看懂执行计划的输出
不同数据库展示执行计划的方式不一样,但核心信息是相通的。我拿Oracle和MySQL举例:
Oracle的执行计划:
--------------------------------------------------------------------
| Id | Operation | Name | Rows | Cost (%CPU)|
--------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 4 (0)|
| 1 | SORT ORDER BY | | 1 | 4 (0)|
| 2 | NESTED LOOPS | | 1 | 3 (0)|
| 3 | TABLE ACCESS FULL| ORDERS | 1 | 2 (0)|
| 4 | TABLE ACCESS BY INDEX| CUSTOMERS| 1 | 1 (0)|
--------------------------------------------------------------------
MySQL的执行计划:
+----+-------------+----------+--------+-----------------+---------+
| id | select_type | table | type | possible_keys | rows |
+----+-------------+----------+--------+-----------------+---------+
| 1 | SIMPLE | orders | ALL | status_idx | 10000 |
| 1 | SIMPLE | customers| eq_ref | PRIMARY | 1 |
+----+-------------+----------+--------+-----------------+---------+
关键字段解读:
- Operation/type:当前操作符和访问路径
- Rows:优化器估算的行数
- Cost:成本估算值(Oracle特有)
- id:缩进越深,执行越早
曾经踩过的坑:有次我看到执行计划里rows估算偏差很大,实际100万行,优化器只估了1000行。结果它选了嵌套循环,跑了半小时。后来收集了统计信息,rows估算准确了,哈希连接2分钟搞定。所以记住:统计信息不准,执行计划就废了。
4.6 本章小结
执行计划是SQL优化的基础。操作符是基本动作,访问路径决定怎么拿数据,连接方法决定怎么拼数据,计划树把这一切组织起来。你想想看,掌握了这些,再看执行计划就像看地图一样清晰。
我个人建议:平时多看看执行计划,特别是那些跑得慢的SQL。看得多了,你自然就能预判数据库会怎么执行你的SQL。这种直觉,比背任何优化技巧都管用。