在我们实际性能调优工作中,执行计划是我们定位SQL语句在数据库中的执行过程或访问路径的描述。达梦执行计划并非凭空产生,而是由数据库优化器基于成本模型计算得出的最优执行方案。核心逻辑可以概括为:
执行计划由多个计划节点组成,每个节点包含以下关键信息:
CSCN2
(聚焦索引扫描)、SSEK2
(二级索引范围扫描)等,表示具体执行的操作。代价,记录行数,字节数
) ,表示该节点的执行成本、输出行数及数据量。语法:
-- 查看单条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)
整个执行计划像一棵树,执行过程为:控制流从上向下传递,数据流从下向上传递。其中三元组例如[1,5,33]这三个数字,分别代表估算的操作符代价、处理的记录行数和每行记录的字节数。
以下是逐行解析:
#NSET2: [1, 5, 33]
[1, 5, 33]
:是统计信息,通常表示估算的操作符代价、处理的记录行数和每行记录的字节数。#PRJT2: [1, 5, 33]; exp_num(3), is_atom(FALSE)
exp_num(3)
:说明投影操作涉及3 个表达式 / 列(即查询需要返回 3 个字段)。is_atom(FALSE)
:表示这不是 “原子投影”。#SLCT2: [1, 5, 33]; (O.ORDER_TIME >= var1 AND O.ORDER_TIME <= var2)
O.ORDER_TIME >= var1 AND O.ORDER_TIME <= var2
,表示筛选出表O
(ORDER_INFO 的别名)中ORDER_TIME
在var1
和var2
之间的记录(范围查询)。#CSCN2: [1, 5, 33]; INDEX33555476(ORDER_INFO as O); btr_scan(1)
INDEX
判断为索引扫描)。INDEX33555476(ORDER_INFO as O)
:说明扫描的是表ORDER_INFO
(别名为O
)上的索引INDEX33555476
(推测是ORDER_TIME
列上的索引,用于加速范围查询)。btr_scan(1)
:表示采用 “B 树扫描(B-tree Scan)” 方式(索引的常见存储结构为 B 树),1
可能表示扫描方向或类型(如正向扫描)。该执行计划的大致流程为:
INDEX33555476
对表ORDER_INFO
进行B树扫描(CSCN2
),获取原始数据;ORDER_TIME
在var1
和var2
之间的记录(SLCT2
);PRJT2
);NSET2
)。CSCN
)SSCN
)ROWID
回表获取完整数据。SSEK/CSEK
)SSEK:
二级索引范围扫描,需回表。CSEK:
聚簇索引范围扫描,直接获取数据。BLKUP
)ROWID
,再回表获取完整数据。NEST LOOP
)嵌套循环连接是最基础的连接方式,将一张表(驱动表)的每一个值与另一张表(被驱动表)的所有值拼接,形成一个大的结果集,再从大结果集中过滤出满足条件的行。驱动表的行数就是循环的次数,将在很大程度上影响执行效率。
WHERE t1.c2='A'
)。idx_t2_c1
)以加速内层查询。HASH JOIN
)哈希连接是在没有索引或者索引无法使用情况下大多数连接的处理方式。哈希连接使用关联列去重后结果集较小的表做成HASH表,另一张表的连接列在HASH后向HASH表进行匹配,这种情况下匹配速度极快,主要开销在于对连接表的全表扫描以及HASH运算。
MERGE JOIN
)归并排序连接需要两张表的连接列都有索引,对两张表扫描索引后按照索引顺序进行归并。
AAGR
)AAGR
用于没有 GROUP BY 的 COUNT、SUM、AGE、MAX、MIN 等聚集函数的计算。
GROUP BY
的聚合函数(如COUNT
、SUM
)。FAGR
)FAGR
用于没有过滤条件时,从表或索引快速获取MAX、MIN、COUNT值。
HAGR
)HAGR 用于分组列没有索引只能走全表扫描的分组聚集,该示例中 C2 列没有创建索引。
SAGR
(流分组聚集)利用索引有序性优化,性能更优。PRJT
是关系的投影计算,用于选择表达式项的计算。广泛用于查询,排序,函数索引创建等。一般没有优化空间。
SLCT
是关系的选择运算,用于查询条件的过滤。可以比较返回结果集与代价估算中是否接近,如相差较大可以考虑收集统计信息。若该过滤条件过滤性较好,可以考虑在条件列增加索引。
NSET 是用于结果集收集的操作符,一般是查询计划的顶层节点,优化工作中无需对该操作符过多关注,一般没有优化空间。
首先我们先准备两张表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条数据
**问题描述:**订单表(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。代价显著降低
问题描述:
部门分组统计
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),避免了哈希表的构建。
CSCN2
)、频繁回表(BLKUP2
)、哈希分组(HAGR2
)是高频痛点,优先通过索引优化;文章
阅读量
获赞