---
title: 分区表- 分区数量多 分区间数据倾斜导致计划走偏-OceanBase数据库使用指南
description: 了解OceanBase数据库在实际应用中关于分区表- 分区数量多 分区间数据倾斜导致计划走偏相关的常见问题和使用技巧，帮助您快速解决分区表- 分区数量多 分区间数据倾斜导致计划走偏的难题。
---
切换语言

- 中文站 - 简体中文
- International - English
- 日本站 - 日本語

划线反馈

# 分区表- 分区数量多 分区间数据倾斜导致计划走偏

更新时间：2026-07-03 06:07

适用版本： V4.2.x、V4.3.x 内容类型：Troubleshoot  

## 问题现象

简单 SQL 查询分区表，默认没有走上正确的索引，导致 SQL 执行慢。表包含 122 个 RANGE 分区（`period_code` 为分区键）。查看统计信息，直方图都准确。

**问题 SQL：**

```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 行。

```shell
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，好的索引由于代价高，被淘汰。

```shell
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` 为表的分区键，计划中可以看出没有进行分区裁剪。

![iamge](https://obbusiness-private.oss-cn-shanghai.aliyuncs.com/doc/img/knowledge-base/database/sql/20250227partition-table-many-partitions-data-skew.png)

查看表的数据分布，发现数据倾斜严重，122 个分区只有 3 个分区有值。

```shell
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，估行偏低，从而导致最终代价计算偏低，被选上。

**如何确认是否走了存储层估行：**

1. **执行计划（EXPLAIN EXTENDED / DBMS_XPLAN）**

   使用 `EXPLAIN EXTENDED` 查看详细计划，或通过 `DBMS_XPLAN.DISPLAY` / `DBMS_XPLAN.DISPLAY_CURSOR` 查看计划时，可在 `Outputs & filters` 区域直接看到估行方式，例如：

   ```shell
   EXPLAIN EXTENDED <问题 SQL>;
   -- 或对已执行 SQL：SELECT DBMS_XPLAN.DISPLAY();

   ```

关注以下信息：

- `estimation method:[OPTIMIZER STATISTICS, STORAGE]` — 表示同时使用了统计信息和存储层估行；若不含 `STORAGE`，则未走存储层估行
 - `EST.ROWS` 是否明显低于实际行数 — 侧面判断估行是否偏低
 - `TABLE RANGE SCAN` 使用的索引名、是否出现分区裁剪 — 辅助判断计划是否走偏

2. **优化器 trace（查看分区级采样明细）**

   需要进一步确认每个采样分区的 `logical rows` / `physical rows`、`storage_est_cost`、`part_cnt_per_dop` 等细节时，再开启 optimizer trace：

   ```shell
   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` — 采样分区估行为 0
 - `part_cnt_per_dop (122.000000)` — LOCAL 索引需跨全部分区，代价被分区数放大

`EXPLAIN EXTENDED` / `DBMS_XPLAN` 输出示例（`estimation method` 行）：

```shell
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 级别。

```shell
-- session 级
set partition_index_dive_limit = xx;
-- global 级
set global partition_index_dive_limit = xx;

```

可以详情参见：[range_index_dive_limit](https://www.oceanbase.com/docs/common-oceanbase-database-cn-1000000001502571) 与 [partition_index_dive_limit](https://www.oceanbase.com/docs/common-oceanbase-database-cn-1000000001502497)。

可以调整 `partition_index_dive_limit` 为分区剪裁后的值，在本例中因为没有分区剪裁，所以为 122。

在调整后不满足需求或拿不准的情况下，也可以将 `partition_index_dive_limit` 调整为 -1。当该参数为 -1 时，走存储层估行的时候，回滚到 10 个分区做存储层估行。但是前提要看是不是走存储层估行。

#### 说明

对于存储层采样的理解，对于剪裁后分区为 20 的场景，如果 `partition_index_dive_limit=10`，只会采样 10 个分区存储层估行，缩放得倒最终结果。

Previous

[指定分区进行旁路导入的使用说明](https://www.oceanbase.com/knowledge-base/oceanbase-database-1000000002339987)

Next

[如何查询分区索引为 local 还是 global](https://www.oceanbase.com/knowledge-base/oceanbase-database-1000000006228288) ![有帮助](https://gw.alipayobjects.com/mdn/ob_asset/afts/img/A*y6ocSqN8cqsAAAAAAAAAAAAAARQnAQ)![无帮助](https://gw.alipayobjects.com/mdn/ob_asset/afts/img/A*BG9IQJyLHF8AAAAAAAAAAAAAARQnAQ)![反馈](https://gw.alipayobjects.com/mdn/ob_asset/afts/img/A*eTWdQKCRKHwAAAAAAAAAAAAAARQnAQ)[AI](https://www.oceanbase.com/obi) 咨询热线
