选择整个分区使用索引 - 为什么?

selecting whole partitions uses index - why?

我正在 select 从分区 table 的某些分区中提取所有数据(Oracle 11g - 实际上在实际情况下更新,但对于示例 select 显示我想了解的相同行为)。你能给我解释一下为什么 Oracle 决定使用索引而不是全扫描吗?以我的理解,全面扫描将是更智能的访问方法。为什么 Oracle 认为索引范围扫描 + table 按索引 rowid 批处理访问比分区的完整扫描更智能?

测试table:

CREATE TABLE t_test
    (ID                NUMBER                 NOT NULL ENABLE, 
    PARTITION_NUMBER   NUMBER                 NOT NULL ENABLE, 
    CREATION_TIMESTAMP DATE   DEFAULT SYSDATE NOT NULL ENABLE, 
    CONSTRAINT PK_t_test PRIMARY KEY (PARTITION_NUMBER, ID) USING INDEX LOCAL
    ) 
PARTITION BY LIST (PARTITION_NUMBER) 
    (
    PARTITION P1  VALUES (1) SEGMENT CREATION IMMEDIATE,
    PARTITION P2  VALUES (2) SEGMENT CREATION IMMEDIATE,
    PARTITION P3  VALUES (3) SEGMENT CREATION IMMEDIATE,
    PARTITION P4  VALUES (4) SEGMENT CREATION IMMEDIATE,
    PARTITION P5  VALUES (5) SEGMENT CREATION IMMEDIATE,
    PARTITION P6  VALUES (6) SEGMENT CREATION IMMEDIATE,
    PARTITION P7  VALUES (7) SEGMENT CREATION IMMEDIATE,
    PARTITION P8  VALUES (8) SEGMENT CREATION IMMEDIATE,
    PARTITION P9  VALUES (9) SEGMENT CREATION IMMEDIATE
    );

从某些分区中选择数据(此示例未插入任何内容,但行为与分区中的数据相同):

explain plan for select * from t_test where PARTITION_NUMBER in (2,3,4,5,6,7);
SELECT * FROM TABLE(dbms_xplan.display);

Plan hash value: 3284178661

-------------------------------------------------------------------------------------------------------------------------
| Id  | Operation                                   | Name      | Rows  | Bytes | Cost (%CPU)| Time     | Pstart| Pstop |
-------------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                            |           |     1 |    35 |     1   (0)| 00:00:01 |       |       |
|   1 |  INLIST ITERATOR                            |           |       |       |            |          |       |       |
|   2 |   PARTITION LIST ITERATOR                   |           |     1 |    35 |     1   (0)| 00:00:01 |KEY(I) |KEY(I) |
|   3 |    TABLE ACCESS BY LOCAL INDEX ROWID BATCHED| T_TEST    |     1 |    35 |     1   (0)| 00:00:01 |KEY(I) |KEY(I) |
|*  4 |     INDEX RANGE SCAN                        | PK_T_TEST |     1 |       |     2   (0)| 00:00:01 |KEY(I) |KEY(I) |
-------------------------------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   4 - access("PARTITION_NUMBER"=2 OR "PARTITION_NUMBER"=3 OR "PARTITION_NUMBER"=4 OR "PARTITION_NUMBER"=5 OR 
              "PARTITION_NUMBER"=6 OR "PARTITION_NUMBER"=7)

Note
-----
   - dynamic statistics used: dynamic sampling (level=2)

我不明白为什么它会进行范围扫描...为什么不像这样(相同的查询但带有 FULL-hint)?为什么它认为在这里使用索引是更好的方法? (即使 ALL_ROWS 提示也不会改变行为):

explain plan for select /*+ full(t_test) */ * from t_test where PARTITION_NUMBER in (2,3,4,5,6,7);
SELECT * FROM TABLE(dbms_xplan.display);

Plan hash value: 3335595461

------------------------------------------------------------------------------------------------
| Id  | Operation             | Name   | Rows  | Bytes | Cost (%CPU)| Time     | Pstart| Pstop |
------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT      |        |     1 |    35 |     2   (0)| 00:00:01 |       |       |
|   1 |  PARTITION LIST INLIST|        |     1 |    35 |     2   (0)| 00:00:01 |KEY(I) |KEY(I) |
|   2 |   TABLE ACCESS FULL   | T_TEST |     1 |    35 |     2   (0)| 00:00:01 |KEY(I) |KEY(I) |
------------------------------------------------------------------------------------------------

Note
-----
   - dynamic statistics used: dynamic sampling (level=2)

因为索引告诉 oracle 您的数据在哪个分区中。当您说 "full scan" 时,您的意思是对 table 进行全面扫描,还是读取指定分区中的所有行?第二个解释计划中的 "TABLE ACCESS FULL" 行在 PstartPstop 列中有 KEY(I),因此 Oracle 只会扫描这些分区中的所有行,而不是整个 table。

所以分区限制只读取 where 子句中的分区,它正在读取特定分区中的所有行并使用索引找出要读取的分区。它使用范围扫描来查找主键中的前导列值。在任一解释计划中,它都使用索引来查看包含指定 PARTITION_NUMBER 的分区——在第一个解释计划中,您看到 4 = access 意味着它正在使用索引。

由于段 space 的分配方式,该示例使用索引而不是完整 table 扫描。默认情况下,Oracle 倾向于为 table 分区分配 non-trivial 数量的 space。对于索引,Oracle 倾向于为索引分区分配更少的 space。

当我 运行 你的示例代码时,空 table 包含 72 兆字节但空索引包含 0.5 兆字节:

select segment_name, sum(bytes)/1024/1024 mb
from user_segments
where segment_name in ('T_TEST', 'PK_T_TEST')
group by segment_name
order by 1 desc;

SEGMENT_NAME   MB
------------   -------
T_TEST         72
PK_T_TEST       0.5625

完整 table 扫描必须读取 整个 段。索引范围扫描仍然需要从 table 读取,但它可以使用 ROWID 来读取,因此从磁盘读取的数据较少。

Oracle 的自动段 space 分配几乎总是比手动配置好。但是对于少量的数据,优化细分可能是有意义的。如果将 9 个 SEGMENT CREATION IMMEDIATE 中的每一个都更改为 STORAGE (INITIAL 64K NEXT 64K),则 table 分区将更小,并且执行计划将使用完整的 table 扫描。但你可能不想那样做。此问题可能仅与使用不切实际的小样本数据集有关。

Oracle 的 space 算法针对在分区中存储大量数据进行了优化。


不幸的是,这可能无法帮助您解决 真正的 问题。您提到了 3 亿行 table,这种大小应该更适合分区。

但真正的问题也可能与段 space 问题有关。也许 table 曾经有 30 亿行,被删除了,但从来没有 sh运行k?或者 table 可能是用数百个分区创建的,其中大部分是空的。或者也许 table 是用一些可笑的手动 space 设置创建的。

在处理大型 table 性能时,我们通常必须考虑段和字节而不是行数。