LabHub
学习 学习路径 课程

SI 的数据库运维

读执行计划,改掉慢查询

在 LabHub 中继续学习

目标

学会阅读执行计划,亲自确认有无索引、列顺序、函数条件和覆盖能力会如何改变计划;把 N+1 改进为连接,并留下调优报告。

为什么重要

阅读执行计划不是浏览节点名称,而是找出优化器估算与实际结果之间的偏差。此外,cost 不是时间,而是相对评分,因此‘cost 超过 10000 就很危险’之类的标准没有依据。 还有一个重要态度:全表扫描并不总是坏事。 当查询需要读取大多数行时,扫描就是最佳方案;看到‘扫描’便条件反射地创建索引,只会增加索引数量并拖慢写入。 本实验将让你一边查看计划,一边亲自练习这些判断。

步骤

  1. 创建 /root/db/tune.db,并执行 /opt/lab/fixtures/dbo/gen.sql 生成 SALES 表。必须有 200,000 行。 列:SALE_IDCUST_IDSALE_DT(字符串 YYYYMMDD)、PROD_CDAMT
  2. 将下方查询(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。 (必须在创建索引之前取得该计划;创建后再取得,计划会不同。)
  3. 为 Q1 创建索引 IX_SALES_01,并将同一查询的计划 保存到 /root/db/plan2.txt。计划中必须出现 IX_SALES_01
  4. 相反的列顺序创建索引 IX_SALES_BAD, 并创建 /root/db/composite.txt。文件包含三行:
    good=<Q1 에 적합한 인덱스의 컬럼 순서, 쉼표 구분>
    bad=<부적합한 순서>
    reason=<한 줄 근거>
    
  5. 将下方查询(Q2)改写成能够使用索引的形式, 并保存到 /root/db/rewrite.sql
    SELECT COUNT(*) FROM SALES WHERE substr(SALE_DT,1,6) = '202603';
    
    改写后查询的结果必须与原查询相同; 将执行计划保存到 /root/db/plan3.txt 时,计划必须使用索引。 (如有需要,可另外创建索引。)
  6. 让下方查询(Q3)只通过覆盖索引完成。
    SELECT SALE_DT, AMT FROM SALES WHERE CUST_ID = 'C000123';
    
    将执行计划保存到 /root/db/covering.txt, 计划中必须出现 COVERING INDEX
  7. 使用 /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 降序排列。 查询必须通过一次连接完成,禁止重复执行子查询。
  8. 编写 /root/db/tuning.md。 必须包含 ## 대상## 증상## 원인## 조치## 결과 五个 h2 标题, 正文中必须包含以下三项内容:
    • 第 2 步与第 3 步计划的差异(SCAN → 索引)
    • ANALYZE 的作用,以及为什么迁移后立即需要执行它
    • 索引会拖慢 INSERT 的原因

参考

创建大规模数据表

创建 /root/db/tune.db,并执行 /opt/lab/fixtures/dbo/gen.sql 生成 SALES 表。必须有 200,000 行。 列:SALE_IDCUST_IDSALE_DT(字符串 YYYYMMDD)、PROD_CDAMT

可以使用递归 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 标题, 正文中必须包含以下三项内容:

这是六个月后有人准备删除该索引时需要阅读的文档。请用数字记录症状、原因、措施和结果。