PARTITION RANGE EMPTY是什么鬼
我同学小何遇到了一个很有趣的问题,在查询一张分区表的时候性能比较差,执行计划竟然出现了PARTITION RANGE EMPTY,这是什么鬼?分区不可用,那么为什么会出现这个呢?
下面是他的SQL语句:
SELECT /*+parallel*/ count(1) cnt FROM xxxxxx partition(P_2017M07) where DATA_DATE= '2017-08-29';
他指定了分区查询的,下面是这条SQL的执行计划:
--------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | Pstart| Pstop | --------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | 40 | 788 (4)| 00:00:10 | | | | 1 | SORT GROUP BY | | 1 | 40 | 788 (4)| 00:00:10 | | | | 2 | NESTED LOOPS | | 1 | 40 | 788 (4)| 00:00:10 | | | | 3 | NESTED LOOPS | | 25 | 40 | 788 (4)| 00:00:10 | | | | 4 | PARTITION RANGE EMPTY | | 1 | 24 | 788 (4)| 00:00:10 |INVALID|INVALID| |* 5 | TABLE ACCESS FULL | xxxxx | 1 | 24 | 788 (4)| 00:00:10 |INVALID|INVALID| |* 6 | INDEX RANGE SCAN | IDX_xxx | 25 | | 2 (0)| 00:00:01 | | | | 7 | TABLE ACCESS BY INDEX ROWID| xxxxxxxxx| 25 | 400 | 5 (0)| 00:00:01 | | | ---------------------------------------------------------------------------------------------------------
他为了证明这是分区表,还发了下这个分区的创建信息,如下:
partition P_2017M07 values less than ('2017-08-01') tablespace TBS_XXX pctfree 10 initrans 1 maxtrans 255 storage ( initial 8M next 1M minextents 1 maxextents unlimited )
其实到这里就已经暴露了导致PARTITION RANGE EMPTY的原因,仔细观察上面的SQL和分区信息就会发现,SQL中指定的时间和这个分区的时间范围是不一致的,也就是说,他查询的时间范围在这个分区是不存在的,这就导致了PARTITION RANGE EMPTY。
为了验证是不是这样,下面进行一下实验,在我的测试库找到一张按天分区的分区表,查询一下分区信息,随便找到一个分区,这里使用SYS_P18983这个分区,查看一下这个分区存的是哪天的数据。
SQL> alter session set nls_date_format='yyyy-mm-dd hh24:mi:ss'; Session altered. SQL> SELECT INST_DTM from TABLE_NAME_XXX PARTITION (SYS_P18983) where rownum=1; INST_DTM ------------------- 2016-10-17 18:07:38
这是按天分区的分区表,这个分区只能存放2016-10-17这一天的数据,查询这个分区并且指定查询时间不是2016-10-17,看下执行计划是不是也会出现PARTITION RANGE EMPTY。
SQL> SELECT COUNT(1) FROM TABLE_NAME_XXX PARTITION (SYS_P18983) WHERE INST_DTM=TO_DATE('2017-08-01 00:00:00','YYYY-MM-DD HH24:MI:SS'); COUNT(1) ---------- 0 Execution Plan ---------------------------------------------------------- Plan hash value: 2534380 --------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | Pstart| Pstop | --------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | 8 | 3 (0)| 00:00:01 | | | | 1 | SORT AGGREGATE | | 1 | 8 | | | | | | 2 | PARTITION RANGE EMPTY| | 1 | 8 | 3 (0)| 00:00:01 |INVALID|INVALID| |* 3 | INDEX RANGE SCAN | INDEX_NAME_XXX | 1 | 8 | 3 (0)| 00:00:01 |INVALID|INVALID| --------------------------------------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 3 - access("INST_DTM"=TO_DATE(' 2017-08-01 00:00:00', 'syyyy-mm-dd hh24:mi:ss')) Statistics ---------------------------------------------------------- 15 recursive calls 0 db block gets 1084 consistent gets 6 physical reads 0 redo size 525 bytes sent via SQL*Net to client 524 bytes received via SQL*Net from client 2 SQL*Net roundtrips to/from client 1 sorts (memory) 0 sorts (disk) 1 rows processed
可以看到,这里也出现了PARTITION RANGE EMPTY,也就验证了,我同学小何遇到这个问题就是因为他查询的SQL使用的时间和指定分区存放的数据不匹配导致的这个问题。
下面看下用正确的时间查询这个分区,正确的执行计划是什么样的。
SQL> SELECT COUNT(1) FROM TABLE_NAME_XXX PARTITION (SYS_P18983) WHERE INST_DTM=SYTO_DATE('2016-10-17 00:00:00','YYYY-MM-DD HH24:MI:SS'); COUNT(1) ---------- 0 Execution Plan ---------------------------------------------------------- Plan hash value: 142327796 ---------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | Pstart| Pstop | ---------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | 8 | 1 (0)| 00:00:01 | | | | 1 | SORT AGGREGATE | | 1 | 8 | | | | | | 2 | PARTITION RANGE SINGLE| | 1 | 8 | 1 (0)| 00:00:01 | 381 | 381 | |* 3 | INDEX RANGE SCAN | INDEX_NAME_XXX | 1 | 8 | 1 (0)| 00:00:01 | 381 | 381 | ---------------------------------------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 3 - access("INST_DTM"=TO_DATE(' 2016-10-17 00:00:00', 'syyyy-mm-dd hh24:mi:ss')) Statistics ---------------------------------------------------------- 18 recursive calls 0 db block gets 43 consistent gets 3 physical reads 0 redo size 525 bytes sent via SQL*Net to client 524 bytes received via SQL*Net from client 2 SQL*Net roundtrips to/from client 13 sorts (memory) 0 sorts (disk) 1 rows processed
通过上面两个SQL的统计信息可以看到,两个SQL都产生了逻辑读和物理读,那么问题来了,分区表的分区列,查询的时间在不在这个分区里,Oracle是知道的,既然知道这个分区不包含这个时间,那么Oracle还会真正扫描这个表或者索引吗?
Oracle当然不会做那些无用功,如果是普通堆表的话,Oracle是不知道查询的数据是否存在的,必须要扫描表或者索引才会知道结果,但对分区表来说,Oracle通过分区的信息是可以直接知道查询的数据分布在具体哪个分区,是否有满足条件的分区,或者指定的分区是否存在要查询的数据,像这种查询的数据并不存在指定的分区的情况,Oracle通过分区信息直接就可以判断出来那个分区没有想要查询的数据,所以就会走PARTITION RANGE EMPTY这个执行计划,直接返回没有查询到数据的结果,并不需要再去扫描分区或者索引。
那么上面的测试两个SQL都产生了逻辑读和物理读是咋回事?我估计是因为这两个SQL都是第一次执行导致的,还有就是AUTOTRACE采集的统计信息是估算值,并不是非常准确。
再次运行正确的SQL,统计信息变成了两个逻辑读。
SQL> SELECT COUNT(1) FROM TABLE_NAME_XXX PARTITION (SYS_P18983) WHERE INST_DTM=TO_DATE('2016-10-17 00:00:00','YYYY-MM-DD HH24:MI:SS'); COUNT(1) ---------- 0 Execution Plan ---------------------------------------------------------- Plan hash value: 142327796 --------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | Pstart| Pstop | --------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | 8 | 1 (0)| 00:00:01 | | | | 1 | SORT AGGREGATE | | 1 | 8 | | | | | | 2 | PARTITION RANGE SINGLE| | 1 | 8 | 1 (0)| 00:00:01 | 381 | 381 | |* 3 | INDEX RANGE SCAN | INDEX_NAME_XXX | 1 | 8 | 1 (0)| 00:00:01 | 381 | 381 | --------------------------------------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 3 - access("INST_DTM"=TO_DATE(' 2016-10-17 00:00:00', 'syyyy-mm-dd hh24:mi:ss')) Statistics ---------------------------------------------------------- 0 recursive calls 0 db block gets 2 consistent gets 0 physical reads 0 redo size 525 bytes sent via SQL*Net to client 524 bytes received via SQL*Net from client 2 SQL*Net roundtrips to/from client 0 sorts (memory) 0 sorts (disk) 1 rows processed
再运行几次统计信息都是一样,两个逻辑读,而执行使用不匹配分区的时间查询,逻辑读和物理读都是0。
SQL> SELECT COUNT(1) FROM TABLE_NAME_XXX PARTITION (SYS_P18983) WHERE INST_DTM=TO_DATE('2017-08-01 00:00:00','YYYY-MM-DD HH24:MI:SS'); COUNT(1) ---------- 0 Execution Plan ---------------------------------------------------------- Plan hash value: 2534380 --------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | Pstart| Pstop | --------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | 8 | 3 (0)| 00:00:01 | | | | 1 | SORT AGGREGATE | | 1 | 8 | | | | | | 2 | PARTITION RANGE EMPTY| | 1 | 8 | 3 (0)| 00:00:01 |INVALID|INVALID| |* 3 | INDEX RANGE SCAN | INDEX_NAME_XXX | 1 | 8 | 3 (0)| 00:00:01 |INVALID|INVALID| --------------------------------------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 3 - access("INST_DTM"=TO_DATE(' 2017-08-01 00:00:00', 'syyyy-mm-dd hh24:mi:ss')) Statistics ---------------------------------------------------------- 0 recursive calls 0 db block gets 0 consistent gets 0 physical reads 0 redo size 525 bytes sent via SQL*Net to client 524 bytes received via SQL*Net from client 2 SQL*Net roundtrips to/from client 0 sorts (memory) 0 sorts (disk) 1 rows processed
多运行几次结果都是一样,这也说明了,在使用不满足范围分区的分区字段进行查询指定分区的时候,Oracle通过分区的特性,可以知道这个分区内不包含要查询的数据,然后直接返回数据没查到的结果,而不会正在去扫描表或者索引。
通过追踪,会发现使用“正确”的时间查询,Oracle底层操作要比使用“不正确”的时间的操作多很多,而且,也会看到使用“正确”的时间查询产生了2个逻辑读,而使用“不正确”的时间查询,并没有产生逻辑读,也就是Oracle发现这个分区没有满足查询条件的数据后,压根就没干活。
但是这些都不是重点,可能已经有人发现了,他的这张表竟然使用的是char类型的字段存放的时间,而且还是用这个char类型的字段做的范围分区,这就导致了他这张分区表,数据分布根本就不是那么回事,大部分数据都在同一个分区内,性能非常低下。