注册
DM执行计划详解与实战优化
专栏/技术分享/ 文章详情 /

DM执行计划详解与实战优化

anon 2025/09/26 759 0 0
摘要

一、执行计划的本质

在我们实际性能调优工作中,执行计划是我们定位SQL语句在数据库中的执行过程或访问路径的描述。达梦执行计划并非凭空产生,而是由数据库优化器基于成本模型计算得出的最优执行方案。核心逻辑可以概括为:

  1. **输入信息:**SQL语句、表结构(字段类型、索引)、统计信息(数据量、值分布)、系统参数(内存大小、并行度)。
  2. **计算过程:**优化器枚举所有可能的执行路径(如全表扫描/索引扫描、不同表连接顺序),计算每条路径的成本(CPU消耗、IO消耗)。
  3. **输出结果:**选择成本最低的路径,生产执行计划。

执行计划由多个计划节点组成,每个节点包含以下关键信息:

  1. **操作符名称:**如CSCN2(聚焦索引扫描)、SSEK2(二级索引范围扫描)等,表示具体执行的操作。
  2. **代价三元组:**格式为(代价,记录行数,字节数) ,表示该节点的执行成本、输出行数及数据量。
  3. **附加信息:**例如索引名、扫描范围、连接条件等,辅助理解操作细节。

二、查看DM执行计划

语法:

-- 查看单条SQL的执行计划 EXPLAIN SELECT O.ORDER_ID,O.USER_ID FROM ORDER_INFO O WHERE O.ORDER_TIME BETWEEN '2024-01-01' AND '2025-01-01';

输出示例解析:

1   #NSET2: [1, 5, 33] 
2     #PRJT2: [1, 5, 33]; exp_num(3), is_atom(FALSE) 
3       #SLCT2: [1, 5, 33]; (O.ORDER_TIME >= var1 AND O.ORDER_TIME <= var2)
4         #CSCN2: [1, 5, 33]; INDEX33555476(ORDER_INFO as O); btr_scan(1)

image.png

整个执行计划像一棵树,执行过程为:控制流从上向下传递,数据流从下向上传递。其中三元组例如[1,5,33]这三个数字,分别代表估算的操作符代价、处理的记录行数和每行记录的字节数。

以下是逐行解析:

1.#NSET2: [1, 5, 33]
  • NSET2:通常表示 “结果集(Result Set)”,是执行计划的最上层,代表最终返回给用户的结果集合。
  • [1, 5, 33]:是统计信息,通常表示估算的操作符代价、处理的记录行数和每行记录的字节数
2.#PRJT2: [1, 5, 33]; exp_num(3), is_atom(FALSE)
  • PRJT2:表示 “投影(Projection)” 操作,即从过滤后的记录中选择需要返回的列(而非返回表中所有列)。
  • exp_num(3):说明投影操作涉及3 个表达式 / 列(即查询需要返回 3 个字段)。
  • is_atom(FALSE):表示这不是 “原子投影”。
3.#SLCT2: [1, 5, 33]; (O.ORDER_TIME >= var1 AND O.ORDER_TIME &lt;= var2)
  • SLCT2:表示 “选择(Selection)” 操作,即根据条件过滤记录。
  • 过滤条件:O.ORDER_TIME >= var1 AND O.ORDER_TIME &lt;= var2,表示筛选出表O(ORDER_INFO 的别名)中ORDER_TIMEvar1var2之间的记录(范围查询)。
4.#CSCN2: [1, 5, 33]; INDEX33555476(ORDER_INFO as O); btr_scan(1)
  • CSCN2:通常表示 “索引扫描(Index Scan)”(具体含义可能因数据库而异,此处结合INDEX判断为索引扫描)。
  • INDEX33555476(ORDER_INFO as O):说明扫描的是表ORDER_INFO(别名为O)上的索引INDEX33555476(推测是ORDER_TIME列上的索引,用于加速范围查询)。
  • btr_scan(1):表示采用 “B 树扫描(B-tree Scan)” 方式(索引的常见存储结构为 B 树),1可能表示扫描方向或类型(如正向扫描)。

该执行计划的大致流程为:

  1. 先通过索引INDEX33555476对表ORDER_INFO进行B树扫描(CSCN2),获取原始数据;
  2. 对扫描结果应用过滤条件,筛选出ORDER_TIMEvar1var2之间的记录(SLCT2);
  3. 从过滤后的记录中投影出需要的3个列(PRJT2);
  4. 最终形成结果集返回(NSET2)。

三、核心操作符分类与解析

(一)表访问操作符

1.聚集索引扫描(CSCN

  • **原理:**扫描聚簇索引的所有数据页,直接获取表数据。
  • 适用场景: 表数据量小、无合适索引或需全表数据时。
  • **优化建议:**高并发场景应尽量避免,可通过添加过滤条件或索引优化。

2.二级索引扫描(SSCN

  • **原理:**扫描二级索引的所有条目,需通过ROWID回表获取完整数据。
  • **适用场景:**需按索引顺序访问数据,但无需过滤条件时。

3.索引范围扫描(SSEK/CSEK

  • **原理:**利用索引快速定位特定范围的数据。
    • SSEK:二级索引范围扫描,需回表。
    • CSEK:聚簇索引范围扫描,直接获取数据。

4.二次扫描(BLKUP

  • **原理:**先通过二级索引定位ROWID,再回表获取完整数据。
  • **优化建议:**若频繁回表,可考虑将常用列加入索引以减少I/O。

(二)连接操作符

1.嵌套循环连接(NEST LOOP

嵌套循环连接是最基础的连接方式,将一张表(驱动表)的每一个值与另一张表(被驱动表)的所有值拼接,形成一个大的结果集,再从大结果集中过滤出满足条件的行。驱动表的行数就是循环的次数,将在很大程度上影响执行效率。

  • **原理:**外层表逐行驱动内层表查询,适用于小表驱动大表。
  • 优化条件:
    • 驱动表有强过滤条件(如WHERE t1.c2='A')。
    • 连接列存在所有(如idx_t2_c1)以加速内层查询。

2.哈希连接(HASH JOIN

哈希连接是在没有索引或者索引无法使用情况下大多数连接的处理方式。哈希连接使用关联列去重后结果集较小的表做成HASH表,另一张表的连接列在HASH后向HASH表进行匹配,这种情况下匹配速度极快,主要开销在于对连接表的全表扫描以及HASH运算。

  • **原理:**将小表构建哈希表,大表通过哈希探测匹配数据。
  • **适用场景:**无索引或索引失效时,大数据量等值连接。

3.合并连接(MERGE JOIN

归并排序连接需要两张表的连接列都有索引,对两张表扫描索引后按照索引顺序进行归并。

  • **原理:**对连接列排序后归并,需两表数据有序。
  • **优化条件:**连接列存在索引且数据分布均匀。

(三)聚合与分组操作符

1.简单聚集(AAGR

AAGR 用于没有 GROUP BY 的 COUNT、SUM、AGE、MAX、MIN 等聚集函数的计算。

  • **原理:**计算无GROUP BY的聚合函数(如COUNTSUM)。

2.快速聚集(FAGR

FAGR用于没有过滤条件时,从表或索引快速获取MAX、MIN、COUNT值。

3.哈希分组聚集(HAGR

HAGR 用于分组列没有索引只能走全表扫描的分组聚集,该示例中 C2 列没有创建索引。

  • **原理:**无索引时通过哈希表实现分组聚合。
  • 对比:SAGR(流分组聚集)利用索引有序性优化,性能更优。

(四)其它关键操作符

1.投影(PRJT)

PRJT是关系的投影计算,用于选择表达式项的计算。广泛用于查询,排序,函数索引创建等。一般没有优化空间。

2.选择(SLCT)

SLCT是关系的选择运算,用于查询条件的过滤。可以比较返回结果集与代价估算中是否接近,如相差较大可以考虑收集统计信息。若该过滤条件过滤性较好,可以考虑在条件列增加索引。

3.结果集收集(NSET)

NSET 是用于结果集收集的操作符,一般是查询计划的顶层节点,优化工作中无需对该操作符过多关注,一般没有优化空间。

四、DM执行计划分析

1.准备工作:

首先我们先准备两张表t_user和表t_order

用户表(t_user)

-- 创建用户表(存储用户基础信息,关联订单表t_order,支持部门分组统计) CREATE TABLE t_user ( user_id BIGINT NOT NULL AUTO_INCREMENT, -- 主键,自增(关联t_order的user_id) user_name VARCHAR(50) NOT NULL, -- 用户名 dept_id INT NOT NULL, -- 部门ID(案例3分组字段) status TINYINT NOT NULL DEFAULT 1, -- 用户状态(1-正常,0-禁用,案例3过滤字段) create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 创建时间 PRIMARY KEY (user_id) -- 主键索引(默认聚集索引) ); -- 注:案例3优化时需创建的索引(对应“哈希分组→流分组”优化) -- CREATE INDEX idx_user_dept_status ON t_user(status, dept_id);

订单表(t_order)

-- 创建订单表(核心业务表,支持时间范围查询、支付状态过滤、关联用户表) CREATE TABLE t_order ( order_id BIGINT NOT NULL AUTO_INCREMENT, -- 订单ID,主键(自增) user_id BIGINT NOT NULL, -- 关联t_user的user_id(外键逻辑) pay_status TINYINT NOT NULL, -- 支付状态(1-已支付,0-未支付,案例2过滤字段) create_time DATETIME NOT NULL, -- 订单创建时间(案例1时间范围过滤字段) order_amount DECIMAL(10,2) NOT NULL, -- 订单金额(扩展字段,可用于统计) PRIMARY KEY (order_id) -- 主键索引(默认聚集索引) ); -- 注1:案例1优化时需创建的覆盖索引(对应“全表扫描→索引扫描”) -- CREATE INDEX idx_order_create_cover ON t_order(create_time) INCLUDE (order_id, user_id); -- 注2:案例2优化时需创建的索引(对应“大表JOIN优化”) -- CREATE INDEX idx_order_pay_create ON t_order(pay_status, create_time) INCLUDE (user_id, order_id);

对这两张表分别插入500w条数据

image.png

image.png

2.案例解析

案例1:全表扫描->索引扫描(DM聚集索引优化)

**问题描述:**订单表(500w行)执行历史订单查询,耗时

SELECT order_id, user_id FROM t_order WHERE create_time BETWEEN '2024-01-01' AND '2024-01-31';

执行计划(优化前):

1   #NSET2: [667, 203700, 41] 
2     #PRJT2: [667, 203700, 41]; exp_num(3), is_atom(FALSE) 
3       #SLCT2: [667, 203700, 41]; (T_ORDER.CREATE_TIME >= var1 AND T_ORDER.CREATE_TIME <= var2)
4         #CSCN2: [667, 5432000, 41]; INDEX33555523(T_ORDER); btr_scan(1)

通过此执行计划可看出,该条SQL在查询时走的是全表扫描。

优化方案:

1.**创建覆盖索引:**在过滤列create_time创建二级索引,包含查询列order_id,user_id

CREATE INDEX idx_order_create_cover ON t_order(create_time,order_id, user_id);

执行计划(优化后)

1   #NSET2: [5, 37500, 41] 
2     #PRJT2: [5, 37500, 41]; exp_num(3), is_atom(FALSE) 
3       #SSEK2: [5, 37500, 41]; scan_type(ASC), IDX_ORDER_CREATE_COVER(T_ORDER), scan_range[(exp_cast('2024-01-01'),min,min),(exp_cast('2024-01-31'),max,max)), is_global(0)

通过优化后的执行计划可以看出,消除了全表扫描CSCN2->SSEK2,走了二级索引范围扫描,且没有回表。扫描的代价从667降低到了5。代价显著降低

案例2:哈希分组->流分组(GROUP BY 优化)

问题描述:

部门分组统计

SELECT dept_id, COUNT(*) FROM t_user WHERE status = 1 GROUP BY dept_id;

执行计划(优化前)

1   #NSET2: [117, 250, 5] 
2     #PRJT2: [117, 250, 5]; exp_num(2), is_atom(FALSE) 
3       #HAGR2: [117, 250, 5]; grp_num(1), sfun_num(1), distinct_flag[0]; slave_empty(0) keys(T_USER.DEPT_ID) 
4         #SLCT2: [115, 25000, 5]; T_USER.STATUS = 1
5           #CSCN2: [115, 1000000, 5]; INDEX33555533(T_USER); btr_scan(1)

通过执行计划,我们可以看出,本条SQL是通过全表扫描+哈希表。代价为117。核心问题是分组列dept_id无所有,触发哈希分组(HAGR2)。

优化方案:

1.创建分组列索引:

CREATE INDEX idx_user_dept_status ON t_user(status, dept_id);

2.利用索引有序性:

索引已按status->dept_id的方式排序,优化器自动选择流分组(SAGR2

执行计划(优化后):

1   #NSET2: [4, 250, 5] 
2     #PRJT2: [4, 250, 5]; exp_num(2), is_atom(FALSE) 
3       #SAGR2: [4, 250, 5]; grp_num(1), sfun_num(1), distinct_flag[0]; slave_empty(0) keys(T_USER.DEPT_ID) 
4         #SSEK2: [3, 25000, 5]; scan_type(ASC), IDX_USER_DEPT_STATUS(T_USER), scan_range[(1,min),(1,max)), is_global(0)

从本次的执行计划可以看出,代价已经降到4,这里利用了索引的有序性,无需哈希分组(HAGR2),优化器自动选择流分组1(SAGR2),避免了哈希表的构建。

五、总结:DM执行计划优化核心思路

  1. 优先消除低效操作:全表扫描(CSCN2)、频繁回表(BLKUP2)、哈希分组(HAGR2)是高频痛点,优先通过索引优化;
  2. 遵循 “小数据量优先”:表连接时控制驱动表行数,分组前过滤数据,减少后续计算量;
  3. 依赖统计信息:执行计划的 “准确性” 取决于统计信息,定期收集大表统计信息;
  4. 结合业务场景:如实时查询优先嵌套循环,批量统计优先哈希连接
评论
后发表回复

作者

文章

阅读量

获赞

扫一扫
联系客服