1. 前言
DBMS_XPLAN 系统包提供了逻辑计划管理相关的功能,包括逻辑计划优化追踪等。非常适合排查单条 SQL 性能问题。下表列出了 OceanBase 数据库当前版本所支持的 DBMS_XPLAN
子程序和简要描述。
子程序 | 描述 |
DISPLAY_ACTIVE_SESSION_PLAN | 用于展示指定 Session 的实时计划详情。 |
DISPLAY_CURSOR | 用于展示已执行的查询计划详情。 |
DISPLAY | 查询并格式化历史的 EXPLAIN 计划。 |
DISABLE_OPT_TRACE | 关闭当前 Session 的优化器全链路追踪功能。 |
DISPLAY_SQL_PLAN_BASELINE | 查看 SPM 的基线计划。 |
ENABLE_OPT_TRACE | 开启当前 Session 的优化器全链路追踪。 |
SET_OPT_TRACE_PARAMETER | 修改当前 Session 的优化器全链路追踪的参数。 |
想了解的大家具体子程序的大家可以点开链接具体去看,本文主要是从给大家提效的角度,帮助大家通过obdiag 快速获取我们一般排查 SQL 性能问题的所需要的DBMS_XPLAN信息,以及如何去解读DBMS_XPLAN的内容。
2. 使用说明
2.1. 安装并配置obdiag
sudo yum install -y yum-utils
sudo yum-config-manager --add-repo https://mirrors.aliyun.com/oceanbase/OceanBase.repo
sudo yum install -y oceanbase-diagnostic-tool
sh /opt/oceanbase-diagnostic-tool/init.sh
# 配置被诊断集群信息
obdiag config -hxx.xx.xx.xx -uroot@sys -Pxxxx -p*****
更多 obdiag 内容参见官网: https://www.oceanbase.com/docs/obdiag-cn
2.2. 命令说明
obdiag gather dbms_xplan [options]
选项说明如下:
选项名 | 是否必选 | 数据类型 | 默认值 | 说明 |
--trace_id | 是 | string | 默认为空 | OceanBase 数据库 V4.0.0 以下版本可从 gv$sql_audit 中查看 trace_id,OceanBase 数据库 V4.0.0 及以上版本可从 gv$ob_sql_audit 中查看 trace_id。 |
--scope | 否 | string | all | 选择收集 SQL 的性能诊断信息时所用的 DBMS_XPLAN 系统包,可配置值如下:opt_tracedisplay_cursor:all |
--user | 是 | string | 默认为空 | 待收集 SQL 的所在租户的用户名。 |
--password | 是 | string | 默认为空 | 待收集 SQL 所在租户的用户密码。 |
--store_dir | 否 | string | 默认为命令执行的当前路径 | 存储结果的本地路径。 |
-c | 否 | string | ~/.obdiag/config.yml | 配置文件路径。 |
--inner_config | 否 | string | 默认为空 | obdiag 自用的配置。 |
--config | 否 | string | 默认为空 | 需被 obdiag 诊断的集群的配置,固定样式:--config key1=value1 --config key2=value2 |
2.3. 举个例子
- 建测试表
create table game (round int primary key, team varchar(10), score int)
partition by hash(round) partitions 3;
insert into game values (1, "CN", 4), (2, "CN", 5), (3, "JP", 3);
insert into game values (4, "CN", 4), (5, "US", 4), (6, "JP", 4);
- 执行并行SQL并获取trace_id
obclient [oceanbase]> select /*+ parallel(3) */ team, sum(score) total from game group by team;
+------+-------+
| team | total |
+------+-------+
| US | 4 |
| CN | 13 |
| JP | 7 |
+------+-------+
3 rows in set (0.006 sec)
obclient [oceanbase]> SELECT last_trace_id();
+-----------------------------------+
| last_trace_id() |
+-----------------------------------+
| YF2A0BA2DA7E-000615B522FD3D35-0-0 |
+-----------------------------------+
1 row in set (0.000 sec)
- obdiag 一键收集dbms_xplan信息
$ obdiag gather dbms_xplan --user=test@sys --password=***** --trace_id=YF2A0BA2DA7E-000615B522FD3D35-0-0
obdiag version: <VERSION>
gather_dbms_xplan start ...
execute dbms_xplan.enable_opt_trace start ...
SET TRANSACTION ISOLATION LEVEL READ COMMITTED
call dbms_xplan.enable_opt_trace();
call dbms_xplan.set_opt_trace_parameter(identifier=>'obdiag_m9IRRY', `level`=>3);
explain select /*+ parallel(3) */ team, sum(score) total from game group by team
call dbms_xplan.disable_opt_trace();
execute dbms_xplan.enable_opt_trace end
Gather dbms_xplan.enable_opt_trace:
+----------------+-----------+---------+--------+----------------------------------------------------------------------------------------------+
| Node | Status | Size | Time | PackPath |
+================+===========+=========+========+==============================================================================================+
| xx.xx.xx.xx | Completed | 35.841K | 0 s | ./obdiag_gather_pack_20250625160759/xx_xx_xx_xx_optimizer_trace_f0dfF2_obdiag_m9IRRY.trac |
+----------------+-----------+---------+--------+----------------------------------------------------------------------------------------------+
Gather dbms_xplan.display_cursor:
+-----------+--------------------------------------------------------------------------+--------+
| Status | Result Details | Time |
+===========+==========================================================================+========+
| Completed | ./obdiag_gather_pack_20250625160759/obdiag_dbms_xplan_display_cursor.txt | 0.39 s |
+-----------+--------------------------------------------------------------------------+--------+
Trace ID: 8262029e-519b-11f0-a748-00163e01c7ce
If you want to view detailed obdiag logs, please run: obdiag display-trace 8262029e-519b-11f0-a748-00163e01c7ce
(base)
可以看到通过obdiag gather dbms_xplan 一条命令就搞定了 dbms_xplan.display_cursor和dbms_xplan.enable_opt_trace的信息, 非常放便,结果统一放到了./obdiag_gather_pack_20250625160759/文件夹中。
2.4. obdiag 执行流程
我们执行一下obdiag display-trace 8262029e-519b-11f0-a748-00163e01c7ce 看下obdiag 的详细日志去看下这个工具怎么拿的信息。
# 看一下dbms_xplan.enable_opt_trace()的内容,关键信息如下:
[2025-06-25 16:07:59.885] [INFO] SET TRANSACTION ISOLATION LEVEL READ COMMITTED
[2025-06-25 16:07:59.885] [INFO] call dbms_xplan.enable_opt_trace();
[2025-06-25 16:08:00.083] [INFO] call dbms_xplan.set_opt_trace_parameter(identifier=>'obdiag_m9IRRY', `level`=>3);
[2025-06-25 16:08:00.096] [INFO] explain select /*+ parallel(3) */ team, sum(score) total from game group by team
[2025-06-25 16:08:00.104] [INFO] call dbms_xplan.disable_opt_trace();
# dbms_xplan.display_cursor 的内容,关键信息如下:
[2025-06-25 16:08:00.135] [DEBUG] - execute SQL: SELECT DBMS_XPLAN.DISPLAY_CURSOR(273900, 'all', '192.168.1.11', 3882, 1) FROM DUAL
[2025-06-25 16:08:00.174] [DEBUG] - dbms_xplan.display_cursor report complete
[2025-06-25 16:08:00.175] [INFO]
[2025-06-25 16:08:00.175] [INFO] Gather dbms_xplan.display_cursor:
[2025-06-25 16:08:00.175] [INFO] +-----------+--------------------------------------------------------------------------+--------+
[2025-06-25 16:08:00.175] [INFO] | Status | Result Details | Time |
[2025-06-25 16:08:00.175] [INFO] +===========+==========================================================================+========+
[2025-06-25 16:08:00.175] [INFO] | Completed | ./obdiag_gather_pack_20250625160759/obdiag_dbms_xplan_display_cursor.txt | 0.39 s |
[2025-06-25 16:08:00.175] [INFO] +-----------+--------------------------------------------------------------------------+--------+
[2025-06-25 16:08:00.175] [INFO]
可以发现,obdiag 工具获取信息的方式和官网提供的一样,省去了大家去记命令的过程。并且大家注意看dbms_xplan.display_cursor的时候,obdiag 用的SQL语句自动填充了参数(273900, 'all', '192.168.1.11', 3882, 1),这些参数从做到右依次是plan_id、format、svr_ip、svr_port、tenant_id。这些参数是obdiag 通过传递的trace_id自动从集群中获取的,别小看了这些参数,这些参数填充才能真正获取刚才执行的SQL的详细dbms_xplan.display_cursor。这里面有个注意点,如果不指定查询计划的话,拿回来的值可能因为执行dbms_xplan.display_cursor的节点和待排查问题SQL执行的节点不是一个导致拿回来的结果不是我们想要的。
SELECT DBMS_XPLAN.DISPLAY_CURSOR(273900, 'all', '192.168.1.11', 3882, 1) FROM DUAL
所以善用工具去执行,还是能省不少事情的。
3. 结果解读
我们收集到两个结果文件
$tree
.
├── xx_xx_xx_xx_optimizer_trace_f0dfF2_obdiag_m9IRRY.trac # 通过 dbms_xplan.enable_opt_trace 收集到的文件
└── obdiag_dbms_xplan_display_cursor.txt # 通过 dbms_xplan.display_cursor 收集到的文件
0 directories, 2 files
3.1. opt_trace 日志解读
理解这部分内容需要具备一定的优化器基础知识,了解 OceanBase 优化器的基础工作原理,包含迭代式的查询改写流程、索引优化、连接枚举、分布式计划优化等。
opt_trace 日志会记录以下信息:
transformer:
- 每个改写规则报告改写前后的 SQL
- 每个改写规则发生改写或不发生改写的详细原因(hint 控制还是什么条件不满足)
optimizer:
- 使用的统计信息
- 基表路径生成日志(包括过程条件、行数、代价估计信息,skyline 剪枝规则过程)
- join order 枚举的详细过程
- top 算子的分配、优化过程
同时日志会在每个模块结束后记录时间和内存开销:
select /*+ PARALLEL(3) */`test`.`game`.`team`,sum(`test`.`game`.`score`) AS `total` from `test`.`game` group by `test`.`game`.`team`
------------------------------------------------------
start prepare mv rewrite info
------------------------------------------------------
table does not have mv, no need to rewrite
-- begin 0 iteration
------------------------------------------------------
start transform rule ObTransformMVRewrite
------------------------------------------------------
transform query block: SEL$1
select /*+ PARALLEL(3) */`test`.`game`.`team`,sum(`test`.`game`.`score`) AS `total` from `test`.`game` group by `test`.`game`.`team`
there is no mv to perform rewrite
transform happened: False
SECTION TIME USAGE: 1338 us
TOTAL TIME USAGE: 1338 us
SECTION MEM USAGE: 240 KB
TOTAL MEM USAGE: 240 KB
- SECTION TIME USAGE 表示从上个优化步骤结束到当前优化步骤结束所使用的时间。
- TOTAL TIME USAGE 表示从查询优化开始到当前优化步骤结束所使用的时间。
- SECTION MEM USAGE 表示从上个优化步骤结束到当前优化步骤结束所使用的租户内存。
- TOTAL MEM USAGE 表示从查询优化开始到当前优化步骤结束所使用的租户内存。
通过这个信息可以快速定位计划生成耗时、耗内存的优化步骤,并且通过使用 hint 关闭对应优化功能,能够快速解决问题,不再需要粗暴的使用 no_rewrite 关闭整个改写来降低计划生成开销。
3.2. display_cursor 结果
完整的结果:
$cat obdiag_dbms_xplan_display_cursor.txt
obclient> SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
call dbms_xplan.enable_opt_trace();
call dbms_xplan.set_opt_trace_parameter(identifier=>'obdiag_m9IRRY', `level`=>3);
explain select /*+ parallel(3) */ team, sum(score) total from game group by team
call dbms_xplan.disable_opt_trace();
obclient> SELECT DBMS_XPLAN.DISPLAY_CURSOR(273900, 'all', 'xx.xx.xx.xx', 3882, 1) FROM DUAL
+------------------------------------------------------------------------------------------------------------------------+
| DBMS_XPLAN.DISPLAY_CURSOR(273900, 'all', 'xx.xx.xx.xx', 3882, 1) |
+------------------------------------------------------------------------------------------------------------------------+
| ====================================================================================================================== |
| |ID|OPERATOR |NAME |EST.ROWS|EST.TIME(us)|REAL.ROWS|REAL.TIME(us)|IO TIME(us)|CPU TIME(us)| |
| ---------------------------------------------------------------------------------------------------------------------- |
| |0 |PX COORDINATOR | |6 |11 |3 |26263 |13728 |20599 | |
| |1 |└─EXCHANGE OUT DISTR |:EX10001|6 |9 |3 |26263 |0 |42 | |
| |2 | └─HASH GROUP BY | |6 |7 |3 |26263 |0 |176 | |
| |3 | └─EXCHANGE IN DISTR | |6 |6 |3 |26263 |6605 |13293 | |
| |4 | └─EXCHANGE OUT DISTR (HASH)|:EX10000|6 |5 |3 |25206 |0 |13081 | |
| |5 | └─HASH GROUP BY | |6 |3 |3 |25206 |0 |162 | |
| |6 | └─PX BLOCK ITERATOR | |6 |3 |6 |17870 |0 |115 | |
| |7 | └─TABLE FULL SCAN |game |6 |3 |6 |8380 |0 |160 | |
| ====================================================================================================================== |
| |
| Outputs & filters: |
| ------------------------------------- |
| 0 - output([INTERNAL_FUNCTION(game.team, T_FUN_SUM(T_FUN_SUM(game.score)))]), filter(nil), rowset=16 |
| 1 - output([INTERNAL_FUNCTION(game.team, T_FUN_SUM(T_FUN_SUM(game.score)))]), filter(nil), rowset=16 |
| dop=3 |
| 2 - output([game.team], [T_FUN_SUM(T_FUN_SUM(game.score))]), filter(nil), rowset=16 |
| group([game.team]), agg_func([T_FUN_SUM(T_FUN_SUM(game.score))]) |
| 3 - output([game.team], [T_FUN_SUM(game.score)]), filter(nil), rowset=16 |
| 4 - output([game.team], [T_FUN_SUM(game.score)]), filter(nil), rowset=16 |
| (#keys=1, [game.team]), dop=3 |
| 5 - output([game.team], [T_FUN_SUM(game.score)]), filter(nil), rowset=16 |
| group([game.team]), agg_func([T_FUN_SUM(game.score)]) |
| 6 - output([game.team], [game.score]), filter(nil), rowset=16 |
| 7 - output([game.team], [game.score]), filter(nil), rowset=16 |
| access([game.team], [game.score]), partitions(p[0-2]) |
| is_index_back=false, is_global_index=false, |
| range_key([game.round]), range(MIN ; MAX)always true |
| |
| Used Hint: |
| ------------------------------------- |
| /*+ |
| |
| PARALLEL(3) |
| */ |
| Qb name trace: |
| ------------------------------------- |
| stmt_id:0, SEL$1 |
| |
| Outline Data: |
| ------------------------------------- |
| /*+ |
| BEGIN_OUTLINE_DATA |
| GBY_PUSHDOWN(@"SEL$1") |
| USE_HASH_AGGREGATION(@"SEL$1") |
| PQ_GBY(@"SEL$1" HASH) |
| PARALLEL(@"SEL$1" "test"."game"@"SEL$1" 3) |
| FULL(@"SEL$1" "test"."game"@"SEL$1") |
| PARALLEL(3) |
| OPTIMIZER_FEATURES_ENABLE('4.3.5.1') |
| END_OUTLINE_DATA |
| */ |
| Optimization Info: |
| ------------------------------------- |
| game: |
| table_rows:6 |
| physical_range_rows:6 |
| logical_range_rows:6 |
| index_back_rows:0 |
| output_rows:6 |
| table_dop:3 |
| dop_method:Global DOP |
| avaiable_index_name:[game] |
| stats info:[version=1970-01-01 08:00:00.000000, is_locked=0, is_expired=0] |
| dynamic sampling level:0 |
| estimation method:[DEFAULT, STORAGE] |
| |
| Plan Type: |
| DISTRIBUTED |
| |
| Note: |
| Degree of Parallelism is 3 because of hint |
| |
| |
+------------------------------------------------------------------------------------------------------------------------+
(base)
- optimizer info 介绍
| Optimization Info: |
| ------------------------------------- |
| game: |
| table_rows:6 |
| physical_range_rows:6 |
| logical_range_rows:6 |
| index_back_rows:0 |
| output_rows:6 |
| table_dop:3 |
| dop_method:Global DOP |
| avaiable_index_name:[game] |
| stats info:[version=1970-01-01 08:00:00.000000, is_locked=0, is_expired=0] |
| dynamic sampling level:0 |
| estimation method:[DEFAULT, STORAGE] |
| |
| Plan Type: |
| DISTRIBUTED |
| |
| Note: |
| Degree of Parallelism is 3 because of hint
属性名称 | 说明 |
table_rows | game 表的原始行数 |
physical_range_rows | game 表在索引上需要扫描的物理行数 |
logical_range_rows | game 表在索引上需要扫描的逻辑行数 |
index_back_rows | game 表需要回表的行数 |
output_rows | game 表经过过滤之后的行数 |
table_dop | game 表扫描的并行度 |
dop_method | 决定表扫描并行度的原因,可以是TableDOP(表定义的并行度)、AutoDop(优化器基于代价选择的并行度,需要打开auto dop功能)、global parallel(parallel hint或系统变量设置的并行度) |
available_index_name | game 表可用的索引列表 |
stats info | game 表统计信息版本号 |
dynamic_sampling_level | 动态采样(一种优化器优化工具,详细介绍请见官网文档)等级,如果值为0,表示该表没有使用动态采样 |
estimation_method | game 表行数估计方式,可以是 DEFAULT(使用的默认统计信息,这种情况行数估计非常不准,需要 DBA 介入优化)、STORAGE(使用存储层实时估行)、STATS(使用统计信息估行) |
Plan Type | 当前计划类型,可以为 LOCAL、REMOTE、DISTRIBUTED |
Note | 生成该计划的一些备注信息,例如:“Degree of Parallelism is 3 because of hint”表示由于当前表的并行度设置为 1,所以当前查询的并行度被设置为 3。 |
3.2.1. (小妙招)快速定位计划性能差的原因
- 找到 CPU TIME 高的 topN 个算子,除去 EXCHANGE IN、EXCHANGE OUT、PX COORD 几个算子,如果有以下算子:
- TABLE SCAN:查看 Output & filter 信息里面是否有回表 is_index_back=true。如果有,关注一下 optimizer info 里面的 index_back_rows,行数比较多的情况下就需要优化下索引。如果 REAL.ROWS 远高于 EST.ROWS,需要关注一下是否收集过统计信息,或者统计信息过期(通过 optimizer info 的 stats version 字段查看)。都有的话就关注一下是否有复杂的过滤条件,例如 case when、like 等等。这种情况下可以通过手动开启动态采样 /+dynamic_sampling(1)/ 来提高估行的准确度。如果这个算子在 Nested Loop Join、SubPlan Filter 算子的右侧,说明是 rescan 太多导致的开销大。
- Nested Loop Join:首先看看左侧算子的行数估计是否正确,如果行数偏差较大,则关注下统计信息问题,或者开启动态采样 /+dynamic_sampling(1)/ 来提高估行的准确度,如果这些都没用就使用 /+use_hash(xxx)/ 来绑定其他计划。如果行数估计正常,性能还是差,需要关注一下 Output & filter 信息,是否使用了 batch_join。
- SubPlan Filter:首先看看左侧算子的行数估计是否正确,如果行数偏差较大,则关注下统计信息问题,或者开启动态采样 /+dynamic_sampling(1)/ 来提高估行的准确度。如果行数估计正常,性能还是差,需要关注一下 Output & filter 信息,是否使用了 batch。如果以上方式都对,需要在 SQL 里面找到对应的子查询,看看能不能改写优化 /+unnest/。
对于其他比较慢的算子,关注一下估行及统计信息的状态,收集统计信息或开启动态采样之后依然没有改善,说明就是数据量比较大,需要开启并行执行 /+parallel(xxx)/,对于 INSERT、UPDATE、DELETE、MERGE 类型算子,需要额外开启 PDML 来优化 /+parallel(xxx) enable_parallel_dml/。
2. 如果注意到 HASH DISTINCT、SORT、HASH GROUP BY、HASH JOIN 等算子有 IO TIME,说明这些算子有落盘的情况,可以适当调整 sql_work_area_size 参数。
4. 写在最后
上面是给大家分享的通过obdiag 来获取DBMS_XPLAN 的信息,非常有助于帮助大家排查SQL性能问题,当然了,大部分情况下排查SQL问题需要的表结构、执行计划等信息可以通过我的另一篇博客:
OceanBase诊断调优 (二十二)—— 并行SQL/慢SQL 问题该如何高效收集诊断信息 这里面拿到的是一份html的报告,基本上足够排查了。
5. 附录
- obdiag 下载地址: https://www.oceanbase.com/softwarecenter
- obdiag 官方文档: https://www.oceanbase.com/docs/obdiag-cn
- obdiag github地址: https://github.com/oceanbase/obdiag
- obdiag SIG 营地: [SIG obdiag] 诊断工具组 · OceanBase 技术交流