首批通过分布式安全可靠测评,为关键业务系统打造
分区表- 分区数量多 分区间数据倾斜导致计划走偏
更新时间:2026-07-03 06:07
问题现象
简单 SQL 查询分区表,默认没有走上正确的索引,导致 SQL 执行慢。表包含 122 个 RANGE 分区(period_code 为分区键)。查看统计信息,直方图都准确。
问题 SQL:
SELECT COUNT(1)
FROM aai_voucher_record
WHERE header_id IN ('2200138856526250216')
AND ytenant_id = 'okq0rf0h'
AND dr = 0
AND send_gl_state IN ('1', '2', '3', '4');
说明
该问题与存储层估行机制有关,V4.x 版本均可能受影响。V4.2.1 BP9 起新增了 range_index_dive_limit 和 partition_index_dive_limit 两个调优参数,可用于缓解此类估行不准问题。本文适用版本请参见文末适用版本章节。
关键诊断信息
下面是一个分析此类问题的例子。
表中包含索引如下,可以看到索引 i_aai_voucher_re_foreignkey 的过滤性比较强,只需要扫描 16 行数据。但是实际执行选择了差的索引 i_aai_voucher_record_ytenant_id,1497056 行。
KEY `i_aai_voucher_record_ytenant_id` (`ytenant_id`) BLOCK_SIZE 16384 LOCAL,
KEY `i_aai_voucher_re_foreignkey` (`header_id`) BLOCK_SIZE 16384 GLOBAL,
MySQL [test]> select count(*) from aai_voucher_record where header_id in ('2200138856526250216');
+----------+
| count(*) |
+----------+
| 16 |
+----------+
1 row in set (0.004 sec)
MySQL [test]> select count(*) from aai_voucher_record where ytenant_id = 'okq0rf0h';
+----------+
| count(*) |
+----------+
| 1497056 |
+----------+
1 row in set (0.295 sec)
查看优化器 trace,好的索引由于代价高,被淘汰。
KEY `i_aai_voucher_record_ytenant_id` (`ytenant_id`) BLOCK_SIZE 16384 LOCAL, 默认索引
KEY `i_aai_voucher_re_foreignkey` (`header_id`) BLOCK_SIZE 16384 GLOBAL, 优的索引
1011769 -> i_aai_voucher_record_ytenant_id 默认索引
1011770 -> i_aai_voucher_re_foreignkey 优的索引
this path is added, interesting path count: 1
calc cost for index: 1011770
io_cost ( 1.674244 ) = first_block_cost ( 1.674244 ) + 1.196038 * ( num_micro_blocks_read ( 1.000000 ) -1)
cpu_cost ( 0.127052 ) = row_count ( 48.000000 ) * 0.002647
cpu_cost ( 2.714818 ) += range_cost ( 2.100000 ) + qual_cost ( 0.000000 ) + project_cost ( 0.487766 )
range_scan_cost ( 2.714818 ) = cpu_cost ( 2.714818 ) + memtable_cost ( 0.000000 ) + memtable_merge_cost ( 0.000000 )
io_cost ( 80.363691 ) = first_block_cost ( 1.674244 ) + 1.674244 * ( num_micro_blocks_read ( 48.000000 ) -1)
cpu_cost ( 0.127052 ) = row_count ( 48.000000 ) * 0.002647
cpu_cost ( 28.981093 ) += range_cost ( 2.100000 ) + qual_cost ( 1.875326 ) + project_cost ( 24.878715 )
fetch_row_cost ( 110.133376 ) = 2.294445 * row_count ( 48.000000 )
range_get_cost ( 219.478160 ) = cpu_cost ( 28.981093 ) + io_cost ( 80.363691 ) + fetch_row_cost ( 110.133376 ) memtable_cost ( 0.000000 ) + memtable_merge_cost ( 0.000000 )
network_cost ( 1175.943834 ) = transform_size ( 14359.304348 ) * 0.011833 + row_count ( 48.000000 ) * 20.959109
index_back_cost ( 1395.421995 ) += network_cost ( 1175.943834 )
cost ( 2.714818 ) += index_scan_cost ( 2.714818 )
cost ( 1398.136813 ) += index_back_cost ( 1395.421995 )
cost ( 1398.136813 ) *= part_cnt_per_dop ( 1.000000 )
cost ( 1398.136813 ) += das_rpc_cost ( 0.000000 )
cost ( 1398.136813 ) = storage_est_cost ( 1398.136813 )
calc cost for index: 1011769
io_cost ( 1.674244 ) = first_block_cost ( 1.674244 ) + 1.196038 * ( num_micro_blocks_read ( 1.000000 ) -1)
cpu_cost ( 0.000022 ) = row_count ( 0.008197 ) * 0.002647
cpu_cost ( 2.100105 ) += range_cost ( 2.100000 ) + qual_cost ( 0.000000 ) + project_cost ( 0.000083 )
range_scan_cost ( 2.100105 ) = cpu_cost ( 2.100105 ) + memtable_cost ( 0.000000 ) + memtable_merge_cost ( 0.000000 )
io_cost ( 1.674244 ) = first_block_cost ( 1.674244 ) + 1.674244 * ( num_micro_blocks_read ( 1.000000 ) -1)
cpu_cost ( 0.000022 ) = row_count ( 0.008197 ) * 0.002647
cpu_cost ( 2.102536 ) += range_cost ( 2.100000 ) + qual_cost ( 0.000320 ) + project_cost ( 0.002194 )
fetch_row_cost ( 0.018807 ) = 2.294445 * row_count ( 0.008197 )
range_get_cost ( 3.795586 ) = cpu_cost ( 2.102536 ) + io_cost ( 1.674244 ) + fetch_row_cost ( 0.018807 ) memtable_cost ( 0.000000 ) + memtable_merge_cost ( 0.000000 )
index_back_cost ( 3.795586 ) += network_cost ( 0.000000 )
cost ( 2.100105 ) += index_scan_cost ( 2.100105 )
cost ( 5.895691 ) += index_back_cost ( 3.795586 )
cost ( 719.274359 ) *= part_cnt_per_dop ( 122.000000 )
cost ( 719.274359 ) += das_rpc_cost ( 0.000000 )
cost ( 719.274359 ) = storage_est_cost ( 719.274359 )
但从实际执行计划可以看出,使用差索引(i_aai_voucher_record_ytenant_id)的实际基表扫描时间比计划高出很多,证明估低了。 且 period_code 为表的分区键,计划中可以看出没有进行分区裁剪。

查看表的数据分布,发现数据倾斜严重,122 个分区只有 3 个分区有值。
MySQL [test]> select count(*) from aai_voucher_record partition(p_aai_voucher_record_min);
+----------+
| count(*) |
+----------+
| 0 |
+----------+
1 row in set (0.007 sec)
MySQL [test]> select count(*) from aai_voucher_record partition(p_aai_voucher_record_202501);
+----------+
| count(*) |
+----------+
| 1155044 |
+----------+
1 row in set (0.004 sec)
MySQL [test]> select count(*) from aai_voucher_record partition(p_aai_voucher_record_202502);
+----------+
| count(*) |
+----------+
| 1920992 |
+----------+
1 row in set (0.028 sec)
MySQL [test]> select count(*) from aai_voucher_record partition(p_aai_voucher_record_202503);
+----------+
| count(*) |
+----------+
| 304 |
+----------+
1 row in set (0.003 sec)
MySQL [test]> select count(*) from aai_voucher_record partition(p_aai_voucher_record_202504);
+----------+
| count(*) |
+----------+
| 0 |
+----------+
1 row in set (0.004 sec)
MySQL [test]> select count(*) from aai_voucher_record partition(p_aai_voucher_record_202505);
+----------+
| count(*) |
+----------+
| 0 |
+----------+
1 row in set (0.004 sec)
对于表本身估行同时使用了 [OPTIMIZER STATISTICS, STORAGE]。优化器 trace 中显示存储层估行,只对 122 种 10 个进行估行(默认上限),估行结果为 0,估行偏低,从而导致最终代价计算偏低,被选上。
如何确认是否走了存储层估行:
执行计划(EXPLAIN EXTENDED / DBMS_XPLAN)
使用
EXPLAIN EXTENDED查看详细计划,或通过DBMS_XPLAN.DISPLAY/DBMS_XPLAN.DISPLAY_CURSOR查看计划时,可在Outputs & filters区域直接看到估行方式,例如:EXPLAIN EXTENDED <问题 SQL>; -- 或对已执行 SQL:SELECT DBMS_XPLAN.DISPLAY();
关注以下信息:
estimation method:[OPTIMIZER STATISTICS, STORAGE]— 表示同时使用了统计信息和存储层估行;若不含STORAGE,则未走存储层估行EST.ROWS是否明显低于实际行数 — 侧面判断估行是否偏低TABLE RANGE SCAN使用的索引名、是否出现分区裁剪 — 辅助判断计划是否走偏
优化器 trace(查看分区级采样明细)
需要进一步确认每个采样分区的
logical rows/physical rows、storage_est_cost、part_cnt_per_dop等细节时,再开启 optimizer trace:SET TRANSACTION ISOLATION LEVEL READ COMMITTED; CALL DBMS_XPLAN.ENABLE_OPT_TRACE(); CALL DBMS_XPLAN.SET_OPT_TRACE_PARAMETER(identifier=>'8461_trace', `level`=>3); EXPLAIN <问题 SQL>; CALL DBMS_XPLAN.DISABLE_OPT_TRACE(); -- 在 observer 日志目录查看 optimizer_trace_*_8461_trace.trac
在 trace 中搜索:
estimation method:[OPTIMIZER STATISTICS, STORAGE]— 与执行计划中的估行方式一致storage_est_cost (...)— 索引路径的存储层代价logical rows: 0 , physical rows: 0— 采样分区估行为 0part_cnt_per_dop (122.000000)— LOCAL 索引需跨全部分区,代价被分区数放大
EXPLAIN EXTENDED / DBMS_XPLAN 输出示例(estimation method 行):
estimation method:[OPTIMIZER STATISTICS, STORAGE]
( index 1011769 , tablet 1152921504607242169 , range (okq0rf0h,MIN,MIN ; okq0rf0h,MAX,MAX) ) logical rows: 0 , physical rows: 0
( index 1011769 , tablet 1152921504607242177 , range (okq0rf0h,MIN,MIN ; okq0rf0h,MAX,MAX) ) logical rows: 0 , physical rows: 0
( index 1011769 , tablet 1152921504607242193 , range (okq0rf0h,MIN,MIN ; okq0rf0h,MAX,MAX) ) logical rows: 0 , physical rows: 0
( index 1011769 , tablet 1152921504607242230 , range (okq0rf0h,MIN,MIN ; okq0rf0h,MAX,MAX) ) logical rows: 0 , physical rows: 0
( index 1011769 , tablet 1152921504607242235 , range (okq0rf0h,MIN,MIN ; okq0rf0h,MAX,MAX) ) logical rows: 0 , physical rows: 0
( index 1011769 , tablet 1152921504607242237 , range (okq0rf0h,MIN,MIN ; okq0rf0h,MAX,MAX) ) logical rows: 0 , physical rows: 0
( index 1011769 , tablet 1152921504607242270 , range (okq0rf0h,MIN,MIN ; okq0rf0h,MAX,MAX) ) logical rows: 0 , physical rows: 0
( index 1011769 , tablet 1152921504607242286 , range (okq0rf0h,MIN,MIN ; okq0rf0h,MAX,MAX) ) logical rows: 0 , physical rows: 0
( index 1011769 , tablet 1152921504607242289 , range (okq0rf0h,MIN,MIN ; okq0rf0h,MAX,MAX) ) logical rows: 0 , physical rows: 0
( index 1011769 , tablet 1152921504607242290 , range (okq0rf0h,MIN,MIN ; okq0rf0h,MAX,MAX) ) logical rows: 0 , physical rows: 0
问题原因
分区表数据倾斜严重,没有进行分区剪裁或者分区剪裁之后的数量大于存储层估行默认值(range_index_dive_limit=10)并且采样的分区数据少,导致代价计算偏低,从而不优的索引被选上。
问题的风险及影响
计划走偏。
影响租户
影响 OceanBase 数据库中的 SYS 租户和 Oracle 租户以及 MySQL 租户。
适用版本
OceanBase 数据库 V4.2.1 BP9(oceanbase-4.2.1.9-109000092024091919)及以后、V4.2.5(oceanbase-4.2.5.0-100000082024102022)、V4.3.5(oceanbase-4.3.5.0-100000122024123020)版本。
解决方法
针对统计信息估行在基表路径选择方面的不足(诸如:实时性不足,数据倾斜处理不够好,多列关联性处理的不好等)的场景,可以通过修改 range_index_dive_limit 和 partition_index_dive_limit 控制参与存储层估行最大的 query range 个数达到优化。
关于该参数可以设置为 session 级或者 global 级别。
-- session 级
set partition_index_dive_limit = xx;
-- global 级
set global partition_index_dive_limit = xx;
可以详情参见:range_index_dive_limit 与 partition_index_dive_limit。
可以调整 partition_index_dive_limit 为分区剪裁后的值,在本例中因为没有分区剪裁,所以为 122。
在调整后不满足需求或拿不准的情况下,也可以将 partition_index_dive_limit 调整为 -1。当该参数为 -1 时,走存储层估行的时候,回滚到 10 个分区做存储层估行。但是前提要看是不是走存储层估行。
说明
对于存储层采样的理解,对于剪裁后分区为 20 的场景,如果 partition_index_dive_limit=10,只会采样 10 个分区存储层估行,缩放得倒最终结果。