注册
DM8 性能优化:统计信息和并行
专栏/技术分享/ 文章详情 /

DM8 性能优化:统计信息和并行

悬铃木 2026/08/21 251 1 0
摘要

DM8 性能优化:统计信息和并行

1. 测试场景与数据说明

1.1 业务模式

测试库模拟了一个典型的销售订单业务系统,围绕“客户下单 → 订单明细 → 支付 → 发货 → 业务事件”组织数据:

  • 客户与商品CUSTOMER(客户)、PRODUCT(商品)、CATEGORY(商品分类)、WAREHOUSE(仓库);
  • 订单链路SALES_ORDER(销售订单)、SALES_ORDER_ITEM(订单明细)、PAYMENT(支付)、SHIPMENT(发货);
  • 消息发布BUSINESS_EVENT(业务事件,模拟 Outbox 模式,待发布事件按时间顺序拉取);
  • 库存INVENTORY(库存),配套 pkg_inventory 包函数做库存预占/释放与审计;
  • 报表分析MIG_BIG_SALES_LINE(千万行销售明细,用于大表聚合)、FACT_SALES(报表事实表)。

本文选取其中三个有代表性的慢 SQL 展开分析,分别对应统计信息、索引/回表、并行三个优化方向。

1.2 三个 Schema 及其职责

Schema 职责
MIG_APP 核心业务表、视图、包函数
MIG_AUDIT 审计(库存流水、触发器审计)
MIG_RPT 报表

1.3 核心表与数据量

对象 行数
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

1.4 关键数据分布

BUSINESS_EVENT.PUBLISH_STATUS 的分布(案例二 Q06 会用到):

状态 行数 占比
NEW 198,008 99.00%
FAILED 2,000 1.00%

1.5 测试方法

  • 使用 JDBC 程序发起查询,Statement.fetchSize=1000
  • 每条 SQL 先执行2 次(消除冷启动、首次磁盘读取的影响),再正式执行 5 次
  • 计时从 JDBC 发出 SQL 开始,到完整读取结果集结束;
  • 5 次执行的平均耗时 作为参考依据。

2. 案例一:统计信息缺失导致计划偏差(Q02 租户客户价值排行)

2.1 查询逻辑

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;

查询逻辑:

  1. 连接:客户表 customer(508 行)左连接订单表 sales_order(20,003 行),一个客户对应多张订单;
  2. 聚合:按客户分组,算出每个客户的订单数 order_count、有效订单金额 lifetime_value(剔除 DRAFT/CANCELLED)、最近下单日期 last_order_date
  3. 窗口排名:用 DENSE_RANK() OVER (PARTITION BY tenant_id ORDER BY lifetime_value DESC) 在每个租户内部按累计消费金额排名,得到 tenant_value_rank
  4. 外层过滤 + 取 Top-100:外层 WHERE tenant_id = 1 只看租户 1,ORDER BY tenant_value_rank, customer_id 排序,FETCH FIRST 100 ROWS ONLY 只取前 100 名。

WHERE tenant_id = 1 实际会命中 254 个客户、约 1 万条订单,过程中存在连接、聚合和窗口排名,性能取决于前三步的执行方式。

2.2 现象与根因分析

优化前 Q02 的 5 次平均耗时为 53.978ms

根因:CUSTOMER.TENANT_ID 列缺少统计信息,优化器严重低估数据量,选择走索引,导致频繁回表和多余排序。

2.3 优化前执行计划

在正式分析前,先介绍 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 张订单”);
  • 而真实情况是 254 个客户、约 1 万条订单,聚合后约 254 组

因为 CUSTOMER.TENANT_ID 列没有统计信息(该列的 NUM_DISTINCT 为空),优化器无法知道“租户 1 有多少客户”,只能按一个很小的默认值估算成 12 行,于是选择了“索引嵌套连接 + 大量回表 + 多层排序”的执行路径。

补充:计划里的 ACTRL备用计划转换控制算子,它会在默认主计划和备用计划之间按实际情况切换。这里是统计信息缺失导致优化器选了一条不适合当前数据量的分支(索引连接 + 回表)。

2.4 优化:收集统计信息

-- 收集表级统计信息 CALL SP_TAB_STAT_INIT('MIG_APP', 'CUSTOMER'); -- 收集关键列(过滤/连接列)统计信息 CALL SP_COL_STAT_INIT('MIG_APP', 'CUSTOMER', 'TENANT_ID');

补上 TENANT_ID 的列统计信息后,优化器就能正确估算租户 1 的客户数。

2.5 优化后执行计划与效果

开启监控后重跑,得到完整执行计划:

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.

从下往上看这条计划:

  1. #CSCN2 [1, 508->508]:全表扫描 CUSTOMER 508 行;
  2. #SLCT2 [1, 254->254]:过滤 TENANT_ID = 1,留下 254 个客户;
  3. #CSCN2 [3, 20003->20003]:全表扫描 SALES_ORDER 20,003 行(PARALLEL scan_type=FULL 表示水平分区表的扫描控制,不是多线程并行);
  4. #HASH LEFT JOIN2 [6, 10161->10004]:Hash 左连接,实际输出约 1 万行(254 个客户的订单);
  5. #HAGR2 [7, 10161->254]:按客户分组聚合,输出 254 行(每个客户一条);
  6. 上层 SORT3AFUN(窗口排名)、PRJT2(投影):完成排序和最终 100 行输出。

优化前 CUSTOMER 走的是 SSEK2 + BLKUP2(索引 + 回表),优化后变成全表扫描 + Hash 连接 + 一次聚合,索引回表路径消失。

效果对比:

状态 5 次平均耗时
收集统计信息前 53.978ms
收集统计信息后 15.666ms

收集统计信息后,平均耗时从 53.978ms 降到 15.666ms。

2.6 分析思路小结

  1. 先看执行计划里每个节点的估算行数(ROW_NUMS# 计划里的 估算->实际),与真实数据量对比,找出估算严重错误的节点;
  2. 对过滤列、连接列检查统计信息是否完整(例如 CUSTOMER.TENANT_IDNUM_DISTINCT 为空即为缺失);
  3. SP_TAB_STAT_INIT / SP_COL_STAT_INIT 补齐统计信息后重看计划;
  4. 若业务高峰期暂不能收集统计信息,可以用 hint 临时指定更合适的访问路径。

3. 案例二:Top-N 查询的索引与回表优化(Q06 待发布业务事件拉取)

3.1 查询逻辑

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 事件表:

  1. 过滤WHERE publish_status = 'NEW' 只取待发布事件(模拟 Outbox 模式里还没被消费的消息);
  2. 排序ORDER BY event_time, event_id 按事件时间(并列时按事件 ID)从早到晚排序,保证“先发生的事件先发布”;
  3. 取 Top-1000FETCH FIRST 1000 ROWS ONLY 每次只拉最早的 1000 条。

数据规模:business_event 共 200,008 行,其中 NEW 占 198,008 行(99%),只有 2,000 行是 FAILED。过滤条件几乎没有选择性;

3.2 现象与根因分析

初测时 Q06 走状态索引回表,5 次平均 225.050ms

执行计划摘要:

SORT3(排序后取 Top-1000) PARALLEL(水平分区扫描,scan_type=FULL) BLKUP2 BUSINESS_EVENT -- 回表取输出列 SSEK2 BUSINESS_EVENT_STATUS_IX -- 状态索引范围扫描 RANGE: publish_status = 'NEW'

分析:

  1. 估算失真:优化器把索引分支估算成约 5,000 行,实际是 198,008 行,严重低估;
  2. 低选择性过滤NEW 占 99%,索引几乎排除不了任何行,全表 99% 的记录都会被命中;
  3. 回表成本BLKUP2 实际输出 198,008 行,说明约 19.8 万条候选记录都要访问基表补列;
  4. 无法提前停止PUBLISH_STATUS='NEW' 是等值条件,前导列 PUBLISH_STATUS 不会破坏其后 EVENT_TIME 的有序性;但原索引缺少第二个排序列 EVENT_ID,只能保证“同一状态内按 EVENT_TIME 有序”,不能完整满足 ORDER BY event_time, event_id,同时也不覆盖其余输出列。

3.3 优化路径一:更新统计信息后改走全表扫描

先更新统计信息,让优化器正确认识“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 万行的过滤,还有继续优化的空间。

3.4 优化路径二:补齐有序索引,提前停止、减少回表

把缺失的第二排序列 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_TYPEAGGREGATE_IDEVENT_TYPE 三列,所以还要为 Top-1000 回表补列,但回表对象从约 19.8 万条候选降到 8451 条,used times:9.494(ms)
  • HPM水平分区表归并排序,把各分区已经有序的结果归并起来。

3.5 优化路径三:覆盖索引,消除回表

如果 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 负责水平分区有序归并。

3.6 分析思路小结

方案 5 次平均耗时 关键变化
原状态索引回表 225.050ms 19.8 万行回表 + SORT3 排序
全表扫描(更新统计信息) 38.8ms 去掉回表,仍有 SORT3 排序
有序索引(补 EVENT_ID) 11.512ms 提前停止,回表降到 8451 条
覆盖索引(消除回表) 3.026ms 无 BLKUP2,直接索引返回

分析思路:

  1. 依旧先定位基数估算失真的节点;
  2. 对比候选路径的逻辑读、回表次数和排序成本——索引不是一定更好,还要看过滤性和是否大量回表
  3. 对 Top-N 查询,检查索引是否满足“等值过滤列 + 完整排序列”;满足时即使过滤选择性很差,也可能扫描到 N 行后提前停止(索引从前往后扫本身就是有序的)。

4. 案例三:大表聚合的并行优化(Q07 千万行销售明细聚合)

4.1 查询逻辑

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,逻辑有三步:

  1. 聚合GROUP BY tenant_id, order_status 按“租户 + 订单状态”分组;
  2. 统计:每组计算行数 COUNT(*)、数量合计 SUM(quantity)、金额合计 SUM(net_amount)、最早/最晚事件时间 MIN/MAX(event_time)
  3. 排序ORDER BY tenant_id, order_status 让结果按租户、状态有序输出。

数据规模:表共 10,100,000 行,分组结果只有 60 行。数据库必须把 1,010 万行全部读一遍并分组累加。

4.2 默认串行执行计划与现象

默认配置下,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 多核资源没有被利用起来。

4.3 开启并行

方式一:会话级开启手动并行 + 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); -- 自动并行下单查询最大并行任务数

4.4 并行执行计划

手动并行度 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 COLLECTLOCAL GATHERLOCAL 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.

从下往上读:

  1. CSCN2 [1372, 10100000->(1262563+...+1262485)]:1,010 万行被 8 路本地并行线程拆分扫描,括号里是每个线程实际扫描的行数(各约 126 万,合计约 1,010 万);
  2. 下层 HAGR2 [1588, 35->(60+60+...+60)]:每个线程先做局部聚合,把自己的分区聚成最多 60 个分组(8 线程 × 60 = 480 条局部结果);
  3. LOCAL DISTRIBUTE在工作线程之间按 TENANT_ID 横向交换数据,把同一个租户的局部结果送到同一个线程,为最终合并做准备(分发前后仍是 480 条,只是重新分派:72+48+72+72+48+48+72+48=480);
  4. 上层 HAGR2 [1588, 35->(9+6+9+9+6+6+9+6)]最终聚合,把相同分组的局部结果合并,各线程分别输出若干组,合计 60 组;
  5. SORT3:各线程对自己负责的几行排序;
  6. LOCAL GATHER:把 8 路有序结果汇集成一路((0+...+60) 表示最终 60 行集中到一个主线程结果流);
  7. PRJT2:投影/计算最终要显示的 7 列;
  8. LOCAL COLLECTfor_sync(TRUE)):等待所有线程结束、关闭并行区域、把结果交给 NSET2 返回。

4.5 分析思路小结

方案 平均耗时 计划路径
基线(默认串行) 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 自动出现并行节点

分析思路与注意事项:

  1. 并行度不是越大越好:生产上应根据 CPU 核数、并发会话数和数据量选择合适的并行度,避免大量小查询并行抢占资源;
  2. MAX_PARALLEL_DEGREE 仅在 PARALLEL_POLICY=1(自动并行)时生效,且应小于等于静态参数 PARALLEL_THRD_NUM;本机 PARALLEL_THRD_NUM=10,设并行度 8 未超过线程池上限;
  3. 并行对大数据量扫描/聚合收益明显(本场景约 3.5s → 0.6s);对毫秒级小查询反而可能因线程调度变慢;
  4. 从业务角度,如果该报表实时性要求不高,也可以建日/月级汇总表预聚合,避免每次扫描 1,010 万行。

5. 优化方法总结

5.1 统计信息

  • 先看估算:执行计划里的估算行数(静态计划的 ROW_NUMS,监控计划的 估算->实际)一旦和真实数据量差距很大,优先怀疑统计信息缺失或过期;
  • 重点检查过滤列、连接列
  • 收集方式
    • SP_TAB_STAT_INIT('模式名', '表名'):收集表级统计信息;
    • SP_COL_STAT_INIT('模式名', '表名', '列名'):收集指定列统计信息;
  • 生产上可在业务低峰期收集统计信息;若暂时不能收集,可用 hint 临时指定访问路径。

5.2 并行

  • 先判断是否真的需要并行:并行适合大表扫描、大聚合、大数据量排序;毫秒级小查询不要硬上并行;
  • 参数
    • PARALLEL_POLICY=0 关闭;=1 自动并行;=2 手动并行(配合 /*+ PARALLEL(表 度) */ hint);
    • MAX_PARALLEL_DEGREE 仅在 PARALLEL_POLICY=1 时作为单查询并行任务数上限,并受 PARALLEL_THRD_NUM 约束;

5.3 本文涉及的操作符

操作符 作用(通俗理解)
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 备用计划转换控制,在主/备用计划之间按实际情况切换
评论
后发表回复

作者

文章

阅读量

获赞

扫一扫
联系客服