读执行计划意味着什么
一句话总结
阅读执行计划,不是浏览节点名称,而是寻找优化器预测与实际结果之间的偏差。
收到了“系统很慢”的报告
稳定期最常收到的报告就是“页面很慢”。此时应按顺序处理。
1. 어느 화면/기능이 느린가 (액세스 로그의 응답시간 상위 URL)
2. 그 기능이 실행하는 SQL 은 무엇인가
3. 그 SQL 의 실행계획은 어떻게 생겼나
4. 옵티마이저의 예측과 실제 결과가 얼마나 다른가
5. 데이터가 문제인가, 인덱스가 문제인가, 쿼리가 문제인가
如果跳过第 1 步,一上来就假设“可能是数据库慢”,方向很容易跑偏。先确定测量点,就是调优的一半。
执行计划不是用来读节点名称的
阅读执行计划,并不是浏览节点名称。 而是寻找优化器预测与实际结果之间的偏差。
优化器根据统计信息预测“这个条件大概会返回多少行”。预测准确时,通常能制定良好计划;预测错误时,就会选出荒谬的计划。因此真正要比较的是预估行数与实际行数。
예측 92,300 실제 91,188 → 오차 1.2%. 통계가 건강하다. 계획을 믿어도 된다
예측 1 실제 482,913 → 옵티마이저가 잘못된 전제 위에서 정확히 계산했다
第二种情况下,需要修复的不是查询,而是统计信息。执行 ANALYZE 更新统计信息后,整个计划可能随之改变。在给查询强行添加提示之前,先怀疑统计信息。
cost 不是时间
另一个常见误解是:执行计划中的 cost 不是时间单位。它以“顺序读取一个磁盘页的成本”为 1.0,表示相对评分。因此,下面这种建议没有依据。
“cost 超过 10000 就很危险。”
10000 可能是 0.3 秒,也可能是 30 秒,取决于硬件与缓存状态。cost 用于比较同一查询的两个计划,不是绝对标准。
父节点时间是累计值
查看执行计划各节点的实际时间时,必须知道一点。
父节点时间是包含所有子节点时间的累计值。
Hash Join (실제 131ms)
├─ Seq Scan on orders (실제 92ms) ← 여기가 진짜 범인
└─ Hash on customers (실제 12ms)
Hash Join 显示 131ms,并不代表连接操作本身很慢,其中 92ms 来自 orders 扫描;连接自身大约只用了 27ms。不理解这一点,就会优化错误的位置。
索引无法生效的典型原因
1. 对列应用了函数
-- 인덱스를 못 탄다
WHERE substr(ORD_DT, 1, 6) = '202608'
-- 범위 조건으로 바꾸면 탄다
WHERE ORD_DT >= '20260801' AND ORD_DT < '20260901'
索引按列的原始值排序。应用函数后就不再是原始值,因此无法利用索引顺序。日期字符串、大小写转换和类型转换中经常发生这种情况。
这类可被索引搜索的条件称为 SARGable。
2. 复合索引的首列没有出现在条件中
对 (CUST_ID, ORD_DT) 索引只给出 WHERE ORD_DT = ?,通常无法有效利用索引。这就像电话簿按姓氏、名字排序,而你只知道名字。
3. 选择度太差
如果条件是 WHERE USE_YN = 'Y',而 99% 的行都是 'Y',那么通过索引定位后再读表,还不如直接扫描全表。优化器不使用索引,可能正是正确判断。
这里最重要的态度是:Seq Scan(全表扫描)并不总是坏事。 对小表,或必须读取大多数行的查询,全表扫描就是最佳方案。看到扫描就下意识加索引,只会让索引越来越多、写入越来越慢。
覆盖索引
如果查询需要的列全部位于索引中,就不必读取表。
-- 인덱스: (CUST_ID, ORD_DT, ORD_AMT)
SELECT ORD_DT, ORD_AMT FROM ORDERS WHERE CUST_ID = ?;
-- → 인덱스만 읽고 끝. 테이블 접근이 없다
执行计划中出现 COVERING INDEX 或 Index Only Scan,就是这种状态。对高频查询,在索引中多放一两个列形成覆盖索引,往往效果显著;代价是索引变大、写入变慢,仍需权衡。
N+1 问题
这是应用层产生的典型性能问题。
주문 100건 조회 → 쿼리 1회
각 주문의 고객명 조회 → 쿼리 100회
총 101회
每条查询只有 1ms,看日志似乎都很快;但往返 101 次后,仅网络延迟就可能达到数百毫秒,而且数据越多,耗时会线性恶化。
解决方式是一次连接查询,或一个 IN 条件。使用 ORM 时,这个问题会悄无声息地出现,因此开发期间应养成习惯:打开 SQL 日志,统计打开一个页面究竟发出多少条查询。
统计信息与 ANALYZE
统计信息是优化器判断的依据,因此统计信息过期,计划就会变差。
- 大量装载或删除后执行
ANALYZE - 迁移后必须执行,否则优化器可能仍认为“这张表是空的”
- 确认是否定期执行;生产数据库中确实存在关闭自动统计收集的情况
大量“迁移后性能突然下降”的报告,根因就是统计信息未更新。查询与索引都没有变化,只有计划改变了;不知道这一点,就会在错误方向上排查很久。
最后——把调优结果写成文档
必须记录改了什么、为什么改。六个月后有人看到“这个奇怪的索引”并打算删除时,这份记录会派上用场。
대상: SCR-021 주문조회
증상: p95 4.2초
원인: (ORD_DT, CUST_ID) 인덱스가 조회 패턴과 순서가 반대
조치: IX_ORDERS_01 (CUST_ID, ORD_DT) 추가, 기존 인덱스 유지(배치가 사용)
결과: p95 0.3초. 실행계획이 스캔에서 인덱스 탐색으로 전환
在实际项目中
从“页面很慢”的报告开始,最常被跳过的就是第 1 步。不先确定测量点,而从“数据库好像很慢”开始,之后所有行动都会建立在猜测之上。从访问日志中找出响应时间最高的 URL 只需一分钟,而这一分钟决定整个方向。
其次常见的是创建了索引,却不知道为什么没用上。原因通常只有三个:对列应用了函数(substr(dt,1,6)='202603')、复合索引的首列不在条件中,或者选择度太差,优化器刻意不使用索引。最后一种情况下强制使用索引,反而会更慢。
调优结果必须形成文档。增加索引并不是免费午餐,而是在读取与写入之间做交换;若没有记录创建原因,后来的人就无法判断它应该删除还是保留。