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的计划树可能是这样的:

执行计划树示例 SORT (ORDER BY) HASH JOIN TABLE ACCESS FULL orders (status='SHIPPED') INDEX UNIQUE SCAN customers (PK) 计划树执行顺序 1. 全表扫描 orders,过滤 status='SHIPPED' 2. 通过主键索引唯一扫描 customers 3. 哈希连接两个结果集 4. 按 order_date 排序输出 注意:叶子节点先执行,根节点最后输出

看懂这棵树,你就能回答这些问题:

  • 数据库先访问哪张表?——看叶子节点,先读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。这种直觉,比背任何优化技巧都管用。