读执行计划,改掉慢查询
目标
学会阅读执行计划,亲自确认有无索引、列顺序、函数条件和覆盖能力会如何改变计划;把 N+1 改进为连接,并留下调优报告。
为什么重要
阅读执行计划不是浏览节点名称,而是找出优化器估算与实际结果之间的偏差。此外,cost 不是时间,而是相对评分,因此‘cost 超过 10000 就很危险’之类的标准没有依据。
还有一个重要态度:全表扫描并不总是坏事。 当查询需要读取大多数行时,扫描就是最佳方案;看到‘扫描’便条件反射地创建索引,只会增加索引数量并拖慢写入。
本实验将让你一边查看计划,一边亲自练习这些判断。
步骤
- 创建
/root/db/tune.db,并执行/opt/lab/fixtures/dbo/gen.sql生成SALES表。必须有 200,000 行。 列:SALE_ID、CUST_ID、SALE_DT(字符串YYYYMMDD)、PROD_CD、AMT - 将下方查询(Q1)的执行计划保存到
/root/db/plan1.txt。
计划中必须出现SELECT SALE_ID, SALE_DT, AMT FROM SALES WHERE CUST_ID = 'C000123' AND SALE_DT BETWEEN '20260101' AND '20260630';SCAN。 (必须在创建索引之前取得该计划;创建后再取得,计划会不同。) - 为 Q1 创建索引
IX_SALES_01,并将同一查询的计划 保存到/root/db/plan2.txt。计划中必须出现IX_SALES_01。 - 以相反的列顺序创建索引
IX_SALES_BAD, 并创建/root/db/composite.txt。文件包含三行:good=<Q1 에 적합한 인덱스의 컬럼 순서, 쉼표 구분> bad=<부적합한 순서> reason=<한 줄 근거> - 将下方查询(Q2)改写成能够使用索引的形式,
并保存到
/root/db/rewrite.sql。
改写后查询的结果必须与原查询相同; 将执行计划保存到SELECT COUNT(*) FROM SALES WHERE substr(SALE_DT,1,6) = '202603';/root/db/plan3.txt时,计划必须使用索引。 (如有需要,可另外创建索引。) - 让下方查询(Q3)只通过覆盖索引完成。
将执行计划保存到SELECT SALE_DT, AMT FROM SALES WHERE CUST_ID = 'C000123';/root/db/covering.txt, 计划中必须出现COVERING INDEX。 - 使用
/opt/lab/fixtures/dbo/gen.sql同时创建的CUSTOMER_M表, 编写查询以求出‘按客户总销售额排名前 10 的客户(包含客户名称)’, 将查询保存到/root/db/join.sql,并将结果保存到/root/db/join-result.txt。 结果包含CUST_ID,CUST_NM,TOTAL三列,共 10 行,按TOTAL降序排列。 查询必须通过一次连接完成,禁止重复执行子查询。 - 编写
/root/db/tuning.md。 必须包含## 대상、## 증상、## 원인、## 조치、## 결과五个 h2 标题, 正文中必须包含以下三项内容:- 第 2 步与第 3 步计划的差异(
SCAN→ 索引) ANALYZE的作用,以及为什么迁移后立即需要执行它- 索引会拖慢 INSERT 的原因
- 第 2 步与第 3 步计划的差异(
参考
- 执行计划:
EXPLAIN QUERY PLAN <쿼리>; - 更新统计信息:
ANALYZE; - 测量时间:
.timer on - 常见错误 1:创建索引后没有执行
ANALYZE,导致计划保持不变。 - 常见错误 2:改写后的查询结果与原查询不同。
substr(SALE_DT,1,6)='202603'表示 3 月 1 日至 3 月 31 日。 - 常见错误 3:为了实现覆盖索引,把所有列都放进索引。 当索引变得和表一样大时,收益便会消失。
创建大规模数据表
创建 /root/db/tune.db,并执行 /opt/lab/fixtures/dbo/gen.sql 生成
SALES 表。必须有 200,000 行。
列:SALE_ID、CUST_ID、SALE_DT(字符串 YYYYMMDD)、PROD_CD、AMT
可以使用递归 CTE 生成大量数据行。创建后请确认行数。
无索引时的执行计划
将下方查询(Q1)的执行计划保存到 /root/db/plan1.txt。
SELECT SALE_ID, SALE_DT, AMT FROM SALES
WHERE CUST_ID = 'C000123' AND SALE_DT BETWEEN '20260101' AND '20260630';
计划中必须出现 SCAN。
(必须在创建索引之前取得该计划;创建后再取得,计划会不同。)
调优的起点是记录改进前的状态。请留意计划中出现了哪些词。
创建索引后进行比较
为 Q1 创建索引 IX_SALES_01,并将同一查询的计划
保存到 /root/db/plan2.txt。计划中必须出现 IX_SALES_01。
观察同一查询的计划如何变化。确认索引名称是否出现在计划中。
复合索引的列顺序
以相反的列顺序创建索引 IX_SALES_BAD,
并创建 /root/db/composite.txt。文件包含三行:
good=<Q1 에 적합한 인덱스의 컬럼 순서, 쉼표 구분>
bad=<부적합한 순서>
reason=<한 줄 근거>
等值条件列应在前,范围条件列应在后。把两种顺序都创建出来并比较计划,差异会非常清楚。
移除函数条件
将下方查询(Q2)改写成能够使用索引的形式,
并保存到 /root/db/rewrite.sql。
SELECT COUNT(*) FROM SALES WHERE substr(SALE_DT,1,6) = '202603';
改写后查询的结果必须与原查询相同;
将执行计划保存到 /root/db/plan3.txt 时,计划必须使用索引。
(如有需要,可另外创建索引。)
对列应用函数后,就无法利用索引的排序顺序。请将其改写为产生相同结果的范围条件。
覆盖索引
让下方查询(Q3)只通过覆盖索引完成。
SELECT SALE_DT, AMT FROM SALES WHERE CUST_ID = 'C000123';
将执行计划保存到 /root/db/covering.txt,
计划中必须出现 COVERING INDEX。
当查询所需的所有列都在索引中时,数据库不必读取表。计划中会额外出现一个特定词组。
用连接取代 N+1
使用 /opt/lab/fixtures/dbo/gen.sql 同时创建的 CUSTOMER_M 表,
编写查询以求出‘按客户总销售额排名前 10 的客户(包含客户名称)’,
将查询保存到 /root/db/join.sql,并将结果保存到 /root/db/join-result.txt。
结果包含 CUST_ID,CUST_NM,TOTAL 三列,共 10 行,按 TOTAL 降序排列。
查询必须通过一次连接完成,禁止重复执行子查询。
将重复查询改写成一次连接。只有结果集与原来相同,才能称为改进。
调优报告
编写 /root/db/tuning.md。
必须包含 ## 대상、## 증상、## 원인、## 조치、## 결과 五个 h2 标题,
正文中必须包含以下三项内容:
- 第 2 步与第 3 步计划的差异(
SCAN→ 索引) ANALYZE的作用,以及为什么迁移后立即需要执行它- 索引会拖慢 INSERT 的原因
这是六个月后有人准备删除该索引时需要阅读的文档。请用数字记录症状、原因、措施和结果。