测试库模拟了一个典型的销售订单业务系统,围绕“客户下单 → 订单明细 → 支付 → 发货 → 业务事件”组织数据:
CUSTOMER(客户)、PRODUCT(商品)、CATEGORY(商品分类)、WAREHOUSE(仓库);SALES_ORDER(销售订单)、SALES_ORDER_ITEM(订单明细)、PAYMENT(支付)、SHIPMENT(发货);BUSINESS_EVENT(业务事件,模拟 Outbox 模式,待发布事件按时间顺序拉取);INVENTORY(库存),配套 pkg_inventory 包函数做库存预占/释放与审计;MIG_BIG_SALES_LINE(千万行销售明细,用于大表聚合)、FACT_SALES(报表事实表)。本文选取其中三个有代表性的慢 SQL 展开分析,分别对应统计信息、索引/回表、并行三个优化方向。
| Schema | 职责 |
|---|---|
MIG_APP |
核心业务表、视图、包函数 |
MIG_AUDIT |
审计(库存流水、触发器审计) |
MIG_RPT |
报表 |
| 对象 | 行数 |
|---|---|
MIG_APP.CUSTOMER |
508 |
MIG_APP.PRODUCT |
40 |
MIG_APP.SALES_ORDER |
20,003 |
MIG_APP.SALES_ORDER_ITEM |
60,005 |
MIG_APP.PAYMENT |
14,001 |
MIG_APP.SHIPMENT |
12,001 |
MIG_APP.BUSINESS_EVENT |
200,008 |
MIG_APP.MIG_BIG_SALES_LINE |
10,100,000 |
MIG_RPT.FACT_SALES |
42,002 |
BUSINESS_EVENT.PUBLISH_STATUS 的分布(案例二 Q06 会用到):
| 状态 | 行数 | 占比 |
|---|---|---|
| NEW | 198,008 | 99.00% |
| FAILED | 2,000 | 1.00% |
Statement.fetchSize=1000;Q02 的 SQL:
SELECT customer_id,
tenant_id,
email,
order_count,
lifetime_value,
TO_CHAR(last_order_date, 'YYYY-MM-DD HH24:MI:SS') AS last_order_time,
tenant_value_rank
FROM MIG_APP.v_customer_order_kpi
WHERE tenant_id = 1
ORDER BY tenant_value_rank, customer_id
FETCH FIRST 100 ROWS ONLY;
Q02 查的是视图 MIG_APP.v_customer_order_kpi,视图定义如下:
CREATE OR REPLACE VIEW MIG_APP.v_customer_order_kpi AS
SELECT c.customer_id, c.tenant_id, c.email, c.full_name,
COUNT(o.order_id) AS order_count,
SUM(CASE WHEN o.order_status NOT IN ('DRAFT','CANCELLED')
THEN o.payable_amount ELSE 0 END) AS lifetime_value,
MAX(o.order_date) AS last_order_date,
DENSE_RANK() OVER (
PARTITION BY c.tenant_id
ORDER BY SUM(CASE WHEN o.order_status NOT IN ('DRAFT','CANCELLED')
THEN o.payable_amount ELSE 0 END) DESC
) AS tenant_value_rank
FROM MIG_APP.customer c
LEFT JOIN MIG_APP.sales_order o ON o.customer_id = c.customer_id
GROUP BY c.customer_id, c.tenant_id, c.email, c.full_name;
查询逻辑:
customer(508 行)左连接订单表 sales_order(20,003 行),一个客户对应多张订单;order_count、有效订单金额 lifetime_value(剔除 DRAFT/CANCELLED)、最近下单日期 last_order_date;DENSE_RANK() OVER (PARTITION BY tenant_id ORDER BY lifetime_value DESC) 在每个租户内部按累计消费金额排名,得到 tenant_value_rank;WHERE tenant_id = 1 只看租户 1,ORDER BY tenant_value_rank, customer_id 排序,FETCH FIRST 100 ROWS ONLY 只取前 100 名。WHERE tenant_id = 1 实际会命中 254 个客户、约 1 万条订单,过程中存在连接、聚合和窗口排名,性能取决于前三步的执行方式。
优化前 Q02 的 5 次平均耗时为 53.978ms。
根因:CUSTOMER.TENANT_ID 列缺少统计信息,优化器严重低估数据量,选择走索引,导致频繁回表和多余排序。
在正式分析前,先介绍 DM8 查看执行计划的两种常用方式,本文后续都沿用这个读法。
方式一:EXPLAIN 静态计划(LEVEL OPERATION 格式)
EXPLAIN <SQL>;
它会输出一棵缩进树,最左列 LEVEL 是层级(0 在最上),缩进表示父子关系,从下往上读,因为数据是先从最底层算子产生、再逐层向上传递的:
OPERATION / OBJECT / INDEX:算子名 + 访问的表或索引;ROW_NUMS:优化器估算本算子向上输出的行数(这是定位统计信息问题的关键列);COST:优化器对该算子的代价估算。方式二:开启节点级监控后的实际执行计划(#NSET2 格式)
SF_SET_SESSION_PARA_VALUE('MONITOR_SQL_EXEC', 1);
SET AUTOTRACE TRACE;
-- 再执行目标 SQL
它会额外给出每个算子的 [代价, 估算行数->实际行数, 行宽],以及 used times(算子实际占用时间)和 n_enter(算子被进入的次数),末尾还有整条 SQL 的 Statistics(逻辑读、物理读、执行时间等)。用它能直接看出估算行数与实际行数的差距,以及时间真正花在哪个算子上。
下面先用静态计划快速定位 Q02 的问题:
LEVEL OPERATION / OBJECT / INDEX ROW_NUMS COST 0 NSET2 1 5 1 PRJT2 1 5 2 SORT3 1 5 3 PRJT2 1 4 4 AFUN 1 4 5 SORT3 1 4 6 HAGR2 1 4 7 PRJT2 25 3 8 HASH LEFT JOIN2 25 3 9 INDEX JOIN LEFT JOIN2 25 3 10 ACTRL 25 3 11 BLKUP2 CUSTOMER / INDEX33560476 12 1 12 SSEK2 CUSTOMER / INDEX33560476 12 1 scan=ASC range=[(CAST(1),min),(CAST(1),max)) 10 PARALLEL scan_type=FULL 2 3 11 BLKUP2 SALES_ORDER / SALES_ORDER_CUSTOMER_DT_IX 2 3 12 SSEK2 SALES_ORDER / SALES_ORDER_CUSTOMER_DT_IX 2 3 scan=ASC range=[(C.CUSTOMER_ID,min),(C.CUSTOMER_ID,max)) 9 PARALLEL scan_type=FULL 20003 3 10 CSCN2 SALES_ORDER / INDEX33560071 20003 3
从这张计划可以看出估算失真:
HAGR2 估算只输出 1 行;HASH LEFT JOIN2 / INDEX JOIN LEFT JOIN2 估算只有 25 行(约等于“12 个客户 × 每个客户 2 张订单”);因为 CUSTOMER.TENANT_ID 列没有统计信息(该列的 NUM_DISTINCT 为空),优化器无法知道“租户 1 有多少客户”,只能按一个很小的默认值估算成 12 行,于是选择了“索引嵌套连接 + 大量回表 + 多层排序”的执行路径。
补充:计划里的
ACTRL是备用计划转换控制算子,它会在默认主计划和备用计划之间按实际情况切换。这里是统计信息缺失导致优化器选了一条不适合当前数据量的分支(索引连接 + 回表)。
-- 收集表级统计信息
CALL SP_TAB_STAT_INIT('MIG_APP', 'CUSTOMER');
-- 收集关键列(过滤/连接列)统计信息
CALL SP_COL_STAT_INIT('MIG_APP', 'CUSTOMER', 'TENANT_ID');
补上 TENANT_ID 的列统计信息后,优化器就能正确估算租户 1 的客户数。
开启监控后重跑,得到完整执行计划:
100 rows got 1 #NSET2: [8, 100->100, 156] ; used times:0.053(ms); n_enter:4 2 #PRJT2: [8, 100->100, 156]; exp_num(7), is_atom(FALSE); INFO_BITS(0); used times:0.026(ms); n_enter:4 3 #SORT3: [8, 100->100, 156]; key_num(2), partition_key_num(0), is_distinct(FALSE), is_adaptive(0), MEM_USED(14336KB), DISK_USED(0KB); used times:0.051(ms); n_enter:4 4 #PRJT2: [7, 10161->254, 156]; exp_num(7), is_atom(FALSE); INFO_BITS(0); used times:0.002(ms); n_enter:4 5 #AFUN: [7, 10161->254, 156]; afun_num(1); used times:0.929(ms); n_enter:4 6 #SORT3: [7, 10161->254, 156]; key_num(2), partition_key_num(0), is_distinct(FALSE), is_adaptive(0), MEM_USED(14336KB), DISK_USED(0KB); used times:0.066(ms); n_enter:4 7 #HAGR2: [7, 10161->254, 156]; grp_num(4), sfun_num(3), MEM_USED(1658KB), DISK_USED(0KB), distinct_flag[0,0,0]; slave_empty(0) keys(DMTEMPVIEW_889202746.TMPCOL0, DMTEMPVIEW_889202746.TMPCOL1, DMTEMPVIEW_889202746.TMPCOL2, DMTEMPVIEW_889202746.TMPCOL3); opt_info_bits(0); used times:3.606(ms); n_enter:64 8 #PRJT2: [6, 10161->10004, 156]; exp_num(7), is_atom(FALSE); INFO_BITS(0); used times:6.886(ms); n_enter:124 9 #HASH LEFT JOIN2: [6, 10161->10004, 156]; key_num(1); col_num(11); partition_keys_num(0); mix(0); MEM_USED(12548KB), DISK_USED(0KB) KEY(C.CUSTOMER_ID=O.CUSTOMER_ID); used times:3.417(ms); n_enter:155 10 #SLCT2: [1, 254->254, 156]; C.TENANT_ID = var6, slct_pushdown(1); used times:0.001(ms); n_enter:6 11 #CSCN2: [1, 508->508, 156]; INDEX33560063(CUSTOMER); btr_scan(1); need_slct(1) prejudge_iescn(0); used times:0.102(ms); n_enter:3 12 #PARALLEL: [3, 20003->20003, 271]; scan_type(FULL,FULL) range_sfun_opt(0); used times:0.064(ms); n_enter:213 13 #CSCN2: [3, 20003->20003, 271]; INDEX33560071(SALES_ORDER); btr_scan(1); need_slct(0) prejudge_iescn(0); used times:4.987(ms); n_enter:123 已用时间: 15.436(毫秒). 执行号:8803.
从下往上看这条计划:
#CSCN2 [1, 508->508]:全表扫描 CUSTOMER 508 行;#SLCT2 [1, 254->254]:过滤 TENANT_ID = 1,留下 254 个客户;#CSCN2 [3, 20003->20003]:全表扫描 SALES_ORDER 20,003 行(PARALLEL scan_type=FULL 表示水平分区表的扫描控制,不是多线程并行);#HASH LEFT JOIN2 [6, 10161->10004]:Hash 左连接,实际输出约 1 万行(254 个客户的订单);#HAGR2 [7, 10161->254]:按客户分组聚合,输出 254 行(每个客户一条);SORT3、AFUN(窗口排名)、PRJT2(投影):完成排序和最终 100 行输出。优化前 CUSTOMER 走的是 SSEK2 + BLKUP2(索引 + 回表),优化后变成全表扫描 + Hash 连接 + 一次聚合,索引回表路径消失。
效果对比:
| 状态 | 5 次平均耗时 |
|---|---|
| 收集统计信息前 | 53.978ms |
| 收集统计信息后 | 15.666ms |
收集统计信息后,平均耗时从 53.978ms 降到 15.666ms。
ROW_NUMS 或 # 计划里的 估算->实际),与真实数据量对比,找出估算严重错误的节点;CUSTOMER.TENANT_ID 的 NUM_DISTINCT 为空即为缺失);SP_TAB_STAT_INIT / SP_COL_STAT_INIT 补齐统计信息后重看计划;Q06 的 SQL:
SELECT event_id,
aggregate_type,
aggregate_id,
event_type,
publish_status
FROM MIG_APP.business_event
WHERE publish_status = 'NEW'
ORDER BY event_time, event_id
FETCH FIRST 1000 ROWS ONLY;
Q06 直接查 MIG_APP.business_event 事件表:
WHERE publish_status = 'NEW' 只取待发布事件(模拟 Outbox 模式里还没被消费的消息);ORDER BY event_time, event_id 按事件时间(并列时按事件 ID)从早到晚排序,保证“先发生的事件先发布”;FETCH FIRST 1000 ROWS ONLY 每次只拉最早的 1000 条。数据规模:business_event 共 200,008 行,其中 NEW 占 198,008 行(99%),只有 2,000 行是 FAILED。过滤条件几乎没有选择性;
初测时 Q06 走状态索引回表,5 次平均 225.050ms。
执行计划摘要:
SORT3(排序后取 Top-1000)
PARALLEL(水平分区扫描,scan_type=FULL)
BLKUP2 BUSINESS_EVENT -- 回表取输出列
SSEK2 BUSINESS_EVENT_STATUS_IX -- 状态索引范围扫描
RANGE: publish_status = 'NEW'
分析:
NEW 占 99%,索引几乎排除不了任何行,全表 99% 的记录都会被命中;BLKUP2 实际输出 198,008 行,说明约 19.8 万条候选记录都要访问基表补列;PUBLISH_STATUS='NEW' 是等值条件,前导列 PUBLISH_STATUS 不会破坏其后 EVENT_TIME 的有序性;但原索引缺少第二个排序列 EVENT_ID,只能保证“同一状态内按 EVENT_TIME 有序”,不能完整满足 ORDER BY event_time, event_id,同时也不覆盖其余输出列。先更新统计信息,让优化器正确认识“NEW 几乎占满整表”这个事实,它会改走全表扫描。
开启监控(MONITOR_SQL_EXEC=1 + SET AUTOTRACE TRACE)后重跑,计划如下(5 次平均 38.8ms):
1000 rows got 1 #NSET2: [44, 1000->1000, 229] ; used times:0.360(ms); n_enter:4 2 #PRJT2: [44, 1000->1000, 229]; exp_num(6), is_atom(FALSE); INFO_BITS(0); used times:0.004(ms); n_enter:4 3 #SORT3: [44, 1000->1000, 229]; key_num(2), partition_key_num(0), is_distinct(FALSE), is_adaptive(0), MEM_USED(14336KB), DISK_USED(0KB); used times:4.515(ms); n_enter:211 4 #PARALLEL: [32, 131341->198008, 229]; scan_type(FULL) range_sfun_opt(0); used times:0.072(ms); n_enter:449 5 #SLCT2: [32, 131341->198008, 229]; BUSINESS_EVENT.PUBLISH_STATUS = 'NEW', slct_pushdown(1); used times:0.046(ms); n_enter:480 6 #CSCN2: [32, 200008->200008, 229]; INDEX33560082(BUSINESS_EVENT); btr_scan(1); need_slct(1) prejudge_iescn(0); used times:32.744(ms); n_enter:240 已用时间: 39.343(毫秒). 执行号:5503.
CSCN2 [32, 200008->200008]:全表扫描 200,008 行,实际输出 200,008 行,used times:32.744(ms) 是本次扫描耗时;SLCT2 [32, 131341->198008]:过滤 PUBLISH_STATUS='NEW',估算 13.1 万、实际 19.8 万(估算仍偏低,但已不影响选路);SORT3:负责 Top-1000 排序,n_enter:211 表示该节点被进入 211 次这一步从 225ms 降到约 39ms,但计划里仍然有 SORT3 和全表 19.8 万行的过滤,还有继续优化的空间。
把缺失的第二排序列 EVENT_ID 补进索引,使索引顺序完整对应“等值过滤列 + 两个 ORDER BY 列”:
CREATE INDEX MIG_APP.BUSINESS_EVENT_STATUS_TIME_ID_IX
ON MIG_APP.BUSINESS_EVENT(PUBLISH_STATUS, EVENT_TIME, EVENT_ID);
监控计划如下(5 次平均 11.512ms):
1000 rows got 1 #NSET2: [1, 1000->1000, 229] ; used times:0.238(ms); n_enter:7 2 #PRJT2: [1, 1000->1000, 229]; exp_num(6), is_atom(FALSE); INFO_BITS(0); used times:0.002(ms); n_enter:10 3 #HPM: [1, 1000->1000, 229]; order_keys(2), is_distinct(0), top_flag(1), pll_scan_type(FULL); used times:0.358(ms); n_enter:40 4 #BLKUP2: [1, 1000->8451, 229]; BUSINESS_EVENT_STATUS_TIME_ID_IX(BUSINESS_EVENT); use_clu_addr(0); used times:9.494(ms); n_enter:70 5 #SSEK2: [1, 1000->8451, 229]; scan_type(ASC), BUSINESS_EVENT_STATUS_TIME_ID_IX(BUSINESS_EVENT), is_global(0), scan_range[('NEW',min,min),('NEW',max,max)); used times:0.401(ms); n_enter:35 已用时间: 11.512(毫秒). 执行号:906.
SSEK2 [1, 1000->8451]:直接按索引顺序范围定位 ('NEW',min,min) ~ ('NEW',max,max)。索引顺序完整匹配“等值列 + 两个排序列”,所以可以按顺序取,不需要先扫全量再排序;BLKUP2 [1, 1000->8451]:该索引没覆盖 AGGREGATE_TYPE、AGGREGATE_ID、EVENT_TYPE 三列,所以还要为 Top-1000 回表补列,但回表对象从约 19.8 万条候选降到 8451 条,used times:9.494(ms);HPM:水平分区表归并排序,把各分区已经有序的结果归并起来。如果 Q06 调用频繁、对延迟要求高,并且能接受更大的索引空间和 DML 维护成本,可以把全部输出列加进索引:
CREATE INDEX MIG_APP.BUSINESS_EVENT_STATUS_TIME_COV_IX
ON MIG_APP.BUSINESS_EVENT(PUBLISH_STATUS, EVENT_TIME, EVENT_ID,
AGGREGATE_TYPE, AGGREGATE_ID, EVENT_TYPE);
监控计划如下(5 次平均 3.026ms):
1000 rows got 1 #NSET2: [1, 1000->1000, 229] ; used times:0.324(ms); n_enter:7 2 #PRJT2: [1, 1000->1000, 229]; exp_num(6), is_atom(FALSE); INFO_BITS(0); used times:0.005(ms); n_enter:10 3 #HPM: [1, 1000->1000, 229]; order_keys(2), is_distinct(0), top_flag(1), pll_scan_type(FULL); used times:0.420(ms); n_enter:40 4 #SSEK2: [1, 1000->8451, 229]; scan_type(ASC), BUSINESS_EVENT_STATUS_TIME_COV_IX(BUSINESS_EVENT), is_global(0), scan_range[('NEW',min,min,min,min,min),('NEW',max,max,max,max,max)); used times:1.395(ms); n_enter:35 已用时间: 3.026(毫秒). 执行号:1904.
SSEK2 [1, 1000->8451]:与路径二相同的索引范围定位,但索引已包含全部输出列,查询所需列都能直接从索引获得;SSEK2,没有 BLKUP2 回表;HPM 负责水平分区有序归并。| 方案 | 5 次平均耗时 | 关键变化 |
|---|---|---|
| 原状态索引回表 | 225.050ms | 19.8 万行回表 + SORT3 排序 |
| 全表扫描(更新统计信息) | 38.8ms | 去掉回表,仍有 SORT3 排序 |
| 有序索引(补 EVENT_ID) | 11.512ms | 提前停止,回表降到 8451 条 |
| 覆盖索引(消除回表) | 3.026ms | 无 BLKUP2,直接索引返回 |
分析思路:
Q07 的 SQL:
SELECT tenant_id,
order_status,
COUNT(*) AS row_count,
SUM(quantity) AS quantity_sum,
SUM(net_amount) AS net_amount_sum,
TO_CHAR(MIN(event_time), 'YYYY-MM-DD HH24:MI:SS.FF6') AS min_event_time,
TO_CHAR(MAX(event_time), 'YYYY-MM-DD HH24:MI:SS.FF6') AS max_event_time
FROM MIG_APP.mig_big_sales_line
GROUP BY tenant_id, order_status
ORDER BY tenant_id, order_status;
Q07 直接查千万行明细表 MIG_APP.mig_big_sales_line,逻辑有三步:
GROUP BY tenant_id, order_status 按“租户 + 订单状态”分组;COUNT(*)、数量合计 SUM(quantity)、金额合计 SUM(net_amount)、最早/最晚事件时间 MIN/MAX(event_time);ORDER BY tenant_id, order_status 让结果按租户、状态有序输出。数据规模:表共 10,100,000 行,分组结果只有 60 行。数据库必须把 1,010 万行全部读一遍并分组累加。
默认配置下,Q07 的 5 次平均为 3,474.635ms。
LEVEL OPERATION / OBJECT / INDEX ROW_NUMS COST 0 NSET2 35 2206 1 PRJT2 35 2206 2 SORT3 35 2206 3 HAGR2 35 2205 4 CSCN2 MIG_BIG_SALES_LINE / INDEX33560094 10100000 1372
CSCN2 全表扫描 1,010 万行 → HAGR2 分组聚合 → SORT3 排序 → 返回 60 行,全程由一个线程完成。
检查会话参数发现,DM8 默认关闭本地并行:
| 参数 | 值 | 含义 |
|---|---|---|
PARALLEL_POLICY |
0 | 0=关闭并行;1=自动并行;2=手动并行(配合 hint) |
MAX_PARALLEL_DEGREE |
1 | 默认 1 表示不并行;仅在 PARALLEL_POLICY=1(自动并行)时作为单查询并行任务数上限 |
在默认配置下,1,010 万行的扫描和聚合都是串行,CPU 多核资源没有被利用起来。
方式一:会话级开启手动并行 + hint
-- 会话级开启手动并行
ALTER SESSION SET 'PARALLEL_POLICY' = 2; -- 2 = 手动并行(配合 hint)
-- 原 SQL 加并行 hint
SELECT /*+ PARALLEL(mig_big_sales_line 8) */
tenant_id,
order_status,
COUNT(*) AS row_count,
SUM(quantity) AS quantity_sum,
SUM(net_amount) AS net_amount_sum,
TO_CHAR(MIN(event_time), 'YYYY-MM-DD HH24:MI:SS.FF6') AS min_event_time,
TO_CHAR(MAX(event_time), 'YYYY-MM-DD HH24:MI:SS.FF6') AS max_event_time
FROM MIG_APP.mig_big_sales_line
GROUP BY tenant_id, order_status
ORDER BY tenant_id, order_status;
方式二:自动并行(不改 SQL,DBA 开启后新会话自动生效)
CALL SP_SET_PARA_VALUE(1, 'PARALLEL_POLICY', 1); -- 1 = 自动并行
CALL SP_SET_PARA_VALUE(1, 'MAX_PARALLEL_DEGREE', 8); -- 自动并行下单查询最大并行任务数
手动并行度 8 的执行计划摘要:
SESSION: PARALLEL_POLICY=2 HINT: PARALLEL(mig_big_sales_line 8) LEVEL OPERATION / OBJECT / INDEX ROW_NUMS COST 0 NSET2 35 1590 1 LOCAL COLLECT 35 1590 2 PRJT2 35 1590 3 LOCAL GATHER 35 1590 4 SORT3 35 1590 5 HAGR2(最终聚合) 35 1588 6 LOCAL DISTRIBUTE 35 1588 7 HAGR2(各线程局部聚合) 35 1588 8 CSCN2 MIG_BIG_SALES_LINE / INDEX33560094 10100000 1372
计划里出现 LOCAL COLLECT、LOCAL GATHER、LOCAL DISTRIBUTE,这是本地并行真正启用的标志
开启监控后的完整执行计划:
60 rows got 1 #NSET2: [1590, 35->60, 151] ; used times:1.265(ms); n_enter:10 2 #LOCAL COLLECT: [1590, 35->(0+0+0+0+0+0+0+60), 151]; op_id(3) n_grp_by (0) n_cols(0) n_keys(0) for_sync(TRUE); used times:0.049(ms); n_enter:39 3 #PRJT2: [1590, 35->(0+0+0+0+0+0+0+60), 151]; exp_num(7), is_atom(FALSE); INFO_BITS(0); used times:0.058(ms); n_enter:32 4 #LOCAL GATHER: [1590, 35->(0+0+0+0+0+0+0+60), 151]; op_id(2) n_grp_by (0) n_cols(7) n_keys(2) invoke_flag(0) top_flag(0) KEY(COL_0,COL_1); used times:0.314(ms); n_enter:32 5 #SORT3: [1590, 35->(9+6+9+9+6+6+9+6), 151]; key_num(2), partition_key_num(0), is_distinct(FALSE), is_adaptive(0), MEM_USED(114688KB), DISK_USED(0KB); used times:0.757(ms); n_enter:32 6 #HAGR2: [1588, 35->(9+6+9+9+6+6+9+6), 151]; grp_num(2), sfun_num(5), MEM_USED(13288KB), DISK_USED(0KB), distinct_flag[0,0,0,0,0]; slave_empty(0) keys(MIG_BIG_SALES_LINE.TENANT_ID, MIG_BIG_SALES_LINE.ORDER_STATUS); opt_info_bits(0); used times:6.044(ms); n_enter:88 7 #LOCAL DISTRIBUTE: [1588, 35->(72+48+72+72+48+48+72+48), 151]; op_id(1) n_keys(0) n_grp(1) flt_only(FALSE) flt_site_data(FALSE) n(0) fbtr_flag(FALSE) KEY(MIG_BIG_SALES_LINE.TENANT_ID); used times:87.865(ms); n_enter:137 8 #HAGR2: [1588, 35->(60+60+60+60+60+60+60+60), 151]; grp_num(2), sfun_num(5), MEM_USED(13296KB), DISK_USED(0KB), distinct_flag[0,0,0,0,0]; slave_empty(0) keys(MIG_BIG_SALES_LINE.TENANT_ID, MIG_BIG_SALES_LINE.ORDER_STATUS); opt_info_bits(0); used times:2389.622(ms); n_enter:13424 9 #CSCN2: [1372, 10100000->(1262563+1262620+1262581+1262539+1262412+1262384+1262416+1262485), 151]; INDEX33560094(MIG_BIG_SALES_LINE); btr_scan(1); need_slct(0) prejudge_iescn(0); used times:2497.984(ms); n_enter:13408 已用时间: 628.339(毫秒). 执行号:1505.
从下往上读:
CSCN2 [1372, 10100000->(1262563+...+1262485)]:1,010 万行被 8 路本地并行线程拆分扫描,括号里是每个线程实际扫描的行数(各约 126 万,合计约 1,010 万);HAGR2 [1588, 35->(60+60+...+60)]:每个线程先做局部聚合,把自己的分区聚成最多 60 个分组(8 线程 × 60 = 480 条局部结果);LOCAL DISTRIBUTE:在工作线程之间按 TENANT_ID 横向交换数据,把同一个租户的局部结果送到同一个线程,为最终合并做准备(分发前后仍是 480 条,只是重新分派:72+48+72+72+48+48+72+48=480);HAGR2 [1588, 35->(9+6+9+9+6+6+9+6)]:最终聚合,把相同分组的局部结果合并,各线程分别输出若干组,合计 60 组;SORT3:各线程对自己负责的几行排序;LOCAL GATHER:把 8 路有序结果汇集成一路((0+...+60) 表示最终 60 行集中到一个主线程结果流);PRJT2:投影/计算最终要显示的 7 列;LOCAL COLLECT(for_sync(TRUE)):等待所有线程结束、关闭并行区域、把结果交给 NSET2 返回。| 方案 | 平均耗时 | 计划路径 |
|---|---|---|
| 基线(默认串行) | 3474.6ms | SORT3 → HAGR2 → CSCN2 全表 |
| 手动并行 hint,并行度 2 | 1751.6ms | 两段 HAGR2 + LOCAL DISTRIBUTE |
| 手动并行 hint,并行度 4 | 1050.1ms | 同上,并行度 4 |
| 手动并行 hint,并行度 8 | 625.7ms | 同上,并行度 8 |
| 自动并行(PARALLEL_POLICY=1)全表扫描 4 | 960.1ms | 自动出现并行节点 |
分析思路与注意事项:
MAX_PARALLEL_DEGREE 仅在 PARALLEL_POLICY=1(自动并行)时生效,且应小于等于静态参数 PARALLEL_THRD_NUM;本机 PARALLEL_THRD_NUM=10,设并行度 8 未超过线程池上限;ROW_NUMS,监控计划的 估算->实际)一旦和真实数据量差距很大,优先怀疑统计信息缺失或过期;SP_TAB_STAT_INIT('模式名', '表名'):收集表级统计信息;SP_COL_STAT_INIT('模式名', '表名', '列名'):收集指定列统计信息;PARALLEL_POLICY=0 关闭;=1 自动并行;=2 手动并行(配合 /*+ PARALLEL(表 度) */ hint);MAX_PARALLEL_DEGREE 仅在 PARALLEL_POLICY=1 时作为单查询并行任务数上限,并受 PARALLEL_THRD_NUM 约束;| 操作符 | 作用(通俗理解) |
|---|---|
CSCN2 |
聚集索引/全表扫描,把整张表的数据读出来 |
SSEK2 |
二级索引定位/范围扫描,按索引键快速找到一段记录 |
BLKUP2 |
二级索引回表,根据二级索引提供的聚集定位信息回到基表取其余列 |
SLCT2 |
过滤(谓词过滤),按 WHERE 条件筛掉不满足的行 |
PRJT2 |
投影/表达式计算,计算最终要输出的列 |
SORT3 |
排序 |
HAGR2 |
Hash 分组聚合(GROUP BY 的分组、SUM/COUNT/MIN/MAX) |
HASH LEFT JOIN2 |
Hash 左外连接 |
AFUN |
窗口函数计算(如 DENSE_RANK) |
HPM |
水平分区表归并排序,把各分区已有序的结果归并 |
PARALLEL |
水平分区子表的扫描控制,描述分区/子分区扫描方式(不是多线程并行) |
LOCAL DISTRIBUTE |
本地并行中工作线程之间按键横向交换数据 |
LOCAL GATHER |
本地并行中把多路结果汇集到一路 |
LOCAL COLLECT |
本地并行区域结束/同步,把结果交给上层 |
ACTRL |
备用计划转换控制,在主/备用计划之间按实际情况切换 |
文章
阅读量
获赞
