Quantcast
Channel: trace – Oracle SQL
Viewing all articles
Browse latest Browse all 16

CBO and Partial indexing

$
0
0

Oracle 12c introduced Partial indexing, which works well for simple partitioned tables with literals. However, it has several significant issues:

For instance, consider the following simple partitioned table:

create table t2 (
  pkey int not null,
  val  int,
  padding varchar2(100)
)
partition by range(pkey) (
  partition p1_on  values less than (2),
  partition p2_on  values less than (3),
  partition p3_off values less than (4) indexing off,
  partition p4_off values less than (5) indexing off,
  partition p5_on  values less than (6),
  partition p6_off values less than (7) indexing off
);
insert into t2
   with pkeys as (select level pkey from dual connect by level<=6)
       ,gen as (select level n from dual connect by level<=1000)
   select pkey,n,rpad('x',100,'x')
   from pkeys,gen;
create index ix_t2 on t2(pkey,val) local indexing partial;
select partition_name as pname,indexing 
from user_tab_partitions p
where table_name='T2';

PNAME      INDEXING
---------- --------
P1_ON      ON
P2_ON      ON
P3_OFF     OFF
P4_OFF     OFF
P5_ON      ON
P6_OFF     OFF

6 rows selected.

This table has 6 partitions (pkey from 1 to 6), each containing 1000 rows with different val (1…1000). And CBO works fine with this simple query:

SQL> select count(*) from t2 where pkey in (1,2,6) and val=5;

  COUNT(*)
----------
         3

SQL> @last ""

PLAN_TABLE_OUTPUT
--------------------------------------------------------------------------------------------------------------------------------------------------
SQL_ID  awkucugb6kwdy, child number 0
-------------------------------------
select count(*) from t2 where pkey in (1,2,6) and val=5

Plan hash value: 3293077569

--------------------------------------------------------------------------------------------------------------------------------------------------
| Id  | Operation                    | Name    | Starts | E-Rows |E-Bytes| Cost (%CPU)| E-Time   | Pstart| Pstop | A-Rows |   A-Time   | Buffers |
--------------------------------------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT             |         |      1 |        |       |   276 (100)|          |       |       |      1 |00:00:00.01 |      88 |
|   1 |  SORT AGGREGATE              |         |      1 |      1 |     7 |            |          |       |       |      1 |00:00:00.01 |      88 |
|   2 |   VIEW                       | VW_TE_2 |      1 |      3 |       |   276   (0)| 00:00:01 |       |       |      3 |00:00:00.01 |      88 |
|   3 |    UNION-ALL                 |         |      1 |        |       |            |          |       |       |      3 |00:00:00.01 |      88 |
|   4 |     INLIST ITERATOR          |         |      1 |        |       |            |          |       |       |      2 |00:00:00.01 |       4 |
|   5 |      PARTITION RANGE ITERATOR|         |      2 |      2 |    14 |     2   (0)| 00:00:01 |KEY(I) |KEY(I) |      2 |00:00:00.01 |       4 |
|*  6 |       INDEX RANGE SCAN       | IX_T2   |      2 |      2 |    14 |     2   (0)| 00:00:01 |KEY(I) |KEY(I) |      2 |00:00:00.01 |       4 |
|   7 |     PARTITION RANGE SINGLE   |         |      1 |      1 |     7 |   274   (0)| 00:00:01 |     6 |     6 |      1 |00:00:00.01 |      84 |
|*  8 |      TABLE ACCESS FULL       | T2      |      1 |      1 |     7 |   274   (0)| 00:00:01 |     6 |     6 |      1 |00:00:00.01 |      84 |
--------------------------------------------------------------------------------------------------------------------------------------------------

Query Block Name / Object Alias (identified by operation id):
-------------------------------------------------------------

   1 - SEL$AE9E49E8
   2 - SET$A73639E0   / VW_TE_2@SEL$AE9E49E8
   3 - SET$A73639E0
   4 - SET$A73639E0_1
   6 - SET$A73639E0_1 / T2@SEL$1
   7 - SET$A73639E0_2
   8 - SET$A73639E0_2 / T2@SEL$1

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

   6 - access((("PKEY"=1 OR "PKEY"=2)) AND "VAL"=5)
   8 - filter(("VAL"=5 AND "PKEY"=6))

As seen above, the CBO opts for the “TABLE EXPANSION” transformation and selects IRS (Index Range Scan) for partitions P1 and P2, and FTS for partition P6. However, when we modify pkey in (1,2,6) to pkey in (1,2), the result is:

select count(*) from t2 where pkey in (1,2) and val=5

Plan hash value: 37798356

------------------------------------------------------------------------------------------------------------------------------------------
| Id  | Operation               | Name | Starts | E-Rows |E-Bytes| Cost (%CPU)| E-Time   | Pstart| Pstop | A-Rows |   A-Time   | Buffers |
------------------------------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT        |      |      1 |        |       |   547 (100)|          |       |       |      1 |00:00:00.01 |     168 |
|   1 |  SORT AGGREGATE         |      |      1 |      1 |     7 |            |          |       |       |      1 |00:00:00.01 |     168 |
|   2 |   PARTITION RANGE INLIST|      |      1 |      2 |    14 |   547   (0)| 00:00:01 |KEY(I) |KEY(I) |      2 |00:00:00.01 |     168 |
|*  3 |    TABLE ACCESS FULL    | T2   |      2 |      2 |    14 |   547   (0)| 00:00:01 |KEY(I) |KEY(I) |      2 |00:00:00.01 |     168 |
------------------------------------------------------------------------------------------------------------------------------------------

This query produces a less efficient plan with 2 FTS, even though it’s simpler and scans one fewer partition than the initial query. Comparing the CBO traces reveals that it skips TABLE EXPANSION transformation in such straightforward cases:

Full test case if you want to play with this example: https://gist.github.com/xtender/5992a9fbf7c088c3bfd68735f75ffdb1

Partial indexing and Composite partitioning

Another issue arises when dealing with partial indexing in conjunction with composite partitioning. The CBO is not yet fully equipped to handle these scenarios.

Consider the following table:

create table t_partial (
  dt    date,
  spkey varchar2(1),
  val   int,
  padding varchar2(100)
)
partition by range (dt)
  subpartition by list (spkey)
     subpartition template ( 
       subpartition p_a values ('A')
     , subpartition p_b values ('B')
     , subpartition p_c values ('C')
     )
(
   partition p_1_on values less than (date'2022-11-02')
  ,partition p_2_on values less than (date'2022-11-03')
  ,partition p_3_off values less than (date'2022-11-04')
  ,partition p_4_off values less than (date'2022-11-05')
  ,partition p_5_on values less than (date'2022-11-06')
)
/
insert into t_partial(dt,spkey,val,padding)
  with dates(dt) as (select date'2022-11-01'+level-1 from dual connect by level<6)
      ,gen as (select n from xmltable('0 to 10000' columns n int path '.'))
  select 
    dt,
    case when n<5000 then 'A'
         when n<10000 then 'C'
         else 'B'
    end as spkey,
    n,
    rpad('x',100,'x') as padding
  from dates,gen
/

The data distribution is as follows:

select dt,spkey,count(*) from t_partial group by dt,spkey order by 1,2
/

DT                  S   COUNT(*)
------------------- - ----------
2022-11-01 00:00:00 A      10000
2022-11-01 00:00:00 B      10000
2022-11-01 00:00:00 C      10000
2022-11-02 00:00:00 A      10000
2022-11-02 00:00:00 B      10000
2022-11-02 00:00:00 C      10000
2022-11-03 00:00:00 A      10000
2022-11-03 00:00:00 B      10000
2022-11-03 00:00:00 C      10000
2022-11-04 00:00:00 A      10000
2022-11-04 00:00:00 B      10000
2022-11-04 00:00:00 C      10000
2022-11-05 00:00:00 A      10000
2022-11-05 00:00:00 B      10000
2022-11-05 00:00:00 C      10000

All subpartitions contain 10000 rows (val from 1 to 10000), and they are currently set to indexing=on:

SQL> col p_name for a10;
SQL> col subp_name for a12;
SQL> col indexing for a8;
SQL> select
  2    partition_position    as p_n
  3   ,partition_name        as p_name
  4   ,subpartition_position as subp_n
  5   ,subpartition_name     as subp_name
  6   ,indexing
  7  from user_tab_subpartitions
  8  where table_name='T_PARTIAL'
  9  /

       P_N P_NAME         SUBP_N SUBP_NAME    INDEXING
---------- ---------- ---------- ------------ --------
         1 P_1_ON              1 P_1_ON_P_A   ON
         1 P_1_ON              2 P_1_ON_P_B   ON
         1 P_1_ON              3 P_1_ON_P_C   ON
         2 P_2_ON              1 P_2_ON_P_A   ON
         2 P_2_ON              2 P_2_ON_P_B   ON
         2 P_2_ON              3 P_2_ON_P_C   ON
         3 P_3_OFF             1 P_3_OFF_P_A  ON
         3 P_3_OFF             2 P_3_OFF_P_B  ON
         3 P_3_OFF             3 P_3_OFF_P_C  ON
         4 P_4_OFF             1 P_4_OFF_P_A  ON
         4 P_4_OFF             2 P_4_OFF_P_B  ON
         4 P_4_OFF             3 P_4_OFF_P_C  ON
         5 P_5_ON              1 P_5_ON_P_A   ON
         5 P_5_ON              2 P_5_ON_P_B   ON
         5 P_5_ON              3 P_5_ON_P_C   ON

Disabling indexing on the partition level is not possible:

SQL> alter table t_partial modify partition P_1_ON indexing on
  2  /
alter table t_partial modify partition P_1_ON indexing on
*
ERROR at line 1:
ORA-14229: cannot modify the indexing attribute of a composite partition

So let’s disable indexing for subpartitions ‘A’ and ‘C’ in partitions P3 and P4 and create a partial index:

SQL> begin
  2   for r in (
  3    select subpartition_name sname
  4    from user_tab_subpartitions
  5    where table_name='T_PARTIAL'
  6      and partition_name like '%OFF'
  7      and subpartition_name not like '%B'
  8   ) loop
  9    execute immediate
 10     'alter table t_partial modify subpartition '||r.sname||' indexing off';
 11   end loop;
 12  end;
 13  /

PL/SQL procedure successfully completed.

SQL> select
  2    partition_position    as p_n
  3   ,partition_name        as p_name
  4   ,subpartition_position as subp_n
  5   ,subpartition_name     as subp_name
  6   ,indexing
  7  from user_tab_subpartitions
  8  where table_name='T_PARTIAL'
  9  /

       P_N P_NAME         SUBP_N SUBP_NAME    INDEXING
---------- ---------- ---------- ------------ --------
         1 P_1_ON              1 P_1_ON_P_A   ON
         1 P_1_ON              2 P_1_ON_P_B   ON
         1 P_1_ON              3 P_1_ON_P_C   ON
         2 P_2_ON              1 P_2_ON_P_A   ON
         2 P_2_ON              2 P_2_ON_P_B   ON
         2 P_2_ON              3 P_2_ON_P_C   ON
         3 P_3_OFF             1 P_3_OFF_P_A  OFF
         3 P_3_OFF             2 P_3_OFF_P_B  ON
         3 P_3_OFF             3 P_3_OFF_P_C  OFF
         4 P_4_OFF             1 P_4_OFF_P_A  OFF
         4 P_4_OFF             2 P_4_OFF_P_B  ON
         4 P_4_OFF             3 P_4_OFF_P_C  OFF
         5 P_5_ON              1 P_5_ON_P_A   ON
         5 P_5_ON              2 P_5_ON_P_B   ON
         5 P_5_ON              3 P_5_ON_P_C   ON

SQL> create index ix_partial on t_partial(spkey,val,dt) local indexing partial
  2  /

Index created.

SQL> exec dbms_stats.gather_table_stats(ownname => user,tabname => 'T_PARTIAL', cascade=>true);

PL/SQL procedure successfully completed.

Now, examine these 2 simple queries, each requesting just 1 day – 2022-11-03 and 2022-11-04:

select count(val) 
from t_partial t 
where spkey='B' and val=3 
and t.dt >= date'2022-11-03' and t.dt < date'2022-11-04';

select count(val) 
from t_partial t 
where spkey='B' and val=3 
and t.dt >= date'2022-11-04' and t.dt < date'2022-11-05';

Both of them produce the same efficient execution plan with IRS:

---------------------------------------------+-----------------------------------+---------------+
| Id  | Operation                | Name      | Rows  | Bytes | Cost  | Time      | Pstart| Pstop |
---------------------------------------------+-----------------------------------+---------------+
| 0   | SELECT STATEMENT         |           |       |       |     2 |           |       |       |
| 1   |  SORT AGGREGATE          |           |     1 |    14 |       |           |       |       |
| 2   |   PARTITION RANGE SINGLE |           |     1 |    14 |     2 |  00:00:01 | 3     | 3     |
| 3   |    PARTITION LIST SINGLE |           |     1 |    14 |     2 |  00:00:01 | 2     | 2     |
| 4   |     INDEX RANGE SCAN     | IX_PARTIAL|     1 |    14 |     2 |  00:00:01 | 8     | 8     |
---------------------------------------------+-----------------------------------+---------------+
Query Block Name / Object Alias (identified by operation id):
------------------------------------------------------------
 1 - SEL$1                
 4 - SEL$1                / "T"@"SEL$1"
------------------------------------------------------------
Predicate Information:
----------------------
4 - access("SPKEY"='B' AND "VAL"=3 AND "T"."DT">=TO_DATE(' 2022-11-03 00:00:00', 'syyyy-mm-dd hh24:mi:ss') AND "T"."DT"<TO_DATE(' 2022-11-04 00:00:00', 'syyyy-mm-dd hh24:mi:ss'))
---------------------------------------------+-----------------------------------+---------------+
| Id  | Operation                | Name      | Rows  | Bytes | Cost  | Time      | Pstart| Pstop |
---------------------------------------------+-----------------------------------+---------------+
| 0   | SELECT STATEMENT         |           |       |       |     2 |           |       |       |
| 1   |  SORT AGGREGATE          |           |     1 |    14 |       |           |       |       |
| 2   |   PARTITION RANGE SINGLE |           |     1 |    14 |     2 |  00:00:01 | 4     | 4     |
| 3   |    PARTITION LIST SINGLE |           |     1 |    14 |     2 |  00:00:01 | 2     | 2     |
| 4   |     INDEX RANGE SCAN     | IX_PARTIAL|     1 |    14 |     2 |  00:00:01 | 11    | 11    |
---------------------------------------------+-----------------------------------+---------------+
Query Block Name / Object Alias (identified by operation id):
------------------------------------------------------------
 1 - SEL$1                
 4 - SEL$1                / "T"@"SEL$1"
------------------------------------------------------------
Predicate Information:
----------------------
4 - access("SPKEY"='B' AND "VAL"=3 AND "T"."DT">=TO_DATE(' 2022-11-04 00:00:00', 'syyyy-mm-dd hh24:mi:ss') AND "T"."DT"<TO_DATE(' 2022-11-05 00:00:00', 'syyyy-mm-dd hh24:mi:ss'))

However, attempting to select both days in a single query results in FTS:

select count(val) 
from t_partial t 
where spkey='B' and val=3 
and t.dt >= date'2022-11-03' and t.dt < date'2022-11-05';

----------------------------------------------+-----------------------------------+---------------+
| Id  | Operation                  | Name     | Rows  | Bytes | Cost  | Time      | Pstart| Pstop |
----------------------------------------------+-----------------------------------+---------------+
| 0   | SELECT STATEMENT           |          |       |       |   548 |           |       |       |
| 1   |  SORT AGGREGATE            |          |     1 |    14 |       |           |       |       |
| 2   |   PARTITION RANGE ITERATOR |          |     2 |    28 |   548 |  00:00:07 | 3     | 4     |
| 3   |    PARTITION LIST SINGLE   |          |     2 |    28 |   548 |  00:00:07 | 2     | 2     |
| 4   |     TABLE ACCESS FULL      | T_PARTIAL|     2 |    28 |   548 |  00:00:07 | KEY   | KEY   |
----------------------------------------------+-----------------------------------+---------------+
Query Block Name / Object Alias (identified by operation id):
------------------------------------------------------------
 1 - SEL$1                
 4 - SEL$1                / "T"@"SEL$1"
------------------------------------------------------------
Predicate Information:
----------------------
4 - filter("VAL"=3)

Comparing their optimizer traces reveals that that CBO disregards the partial index in case of partition range iterator, if at least 1 subpartition in that range is “unusable”:

Additionally, the same issue occurs with composite partitioning when using bind variables: CBO ignores partial indexes when bind variables are used, even if there is only 1 partition, as in the first example with literals:

declare
 dt_1 date:=date'2022-11-03';
 dt_2 date:=date'2022-11-04';
 cnt int;
begin
  select count(val)
    into cnt 
  from t_partial t 
  where spkey='B' and val=3 and t.dt >= dt_1 and t.dt < dt_2;
end;
/
Index Stats::
  Index: IX_PARTIAL  Col#: 2 3 1  PARTITION [2]
  LVLS: 1  #LB: 35  #DK: 10000  LB/K: 1.00  DB/K: 1.00  CLUF: 170.00  NRW: 10000.00 SSZ: 10000.00 LGR: 0.00 CBK: 0.00 GQL: 100.00 CHR: 0.00 KQDFLG: 0 BSZ: 1
  KKEISFLG: 1 
  LVLS: 1  #LB: 35  #DK: 10000  LB/K: 1.00  DB/K: 1.00  CLUF: 170.00  NRW: 10000.00 SSZ: 10000.00 LGR: 0.00 CBK: 0.00 GQL: 100.00 CHR: 0.00 KQDFLG: 0 BSZ: 1
  KKEISFLG: 1 
    UNUSABLE

----- Current SQL Statement for this session (sql_id=3xj8t99km1zkz) -----
SELECT COUNT(VAL) FROM T_PARTIAL T WHERE SPKEY='B' AND VAL=3 AND T.DT >= :B2 AND T.DT < :B1 
----- PL/SQL Stack -----
----- PL/SQL Call Stack -----
  object      line  object
  handle    number  name
0x75dd2cc0         6  anonymous block
sql_text_length=93
sql=SELECT COUNT(VAL) FROM T_PARTIAL T WHERE SPKEY='B' AND VAL=3 AND T.DT >= :B2 AND T.DT < :B1 
----- Explain Plan Dump -----
----- Plan Table -----
 
============
Plan Table
============
-----------------------------------------------+-----------------------------------+---------------+
| Id  | Operation                   | Name     | Rows  | Bytes | Cost  | Time      | Pstart| Pstop |
-----------------------------------------------+-----------------------------------+---------------+
| 0   | SELECT STATEMENT            |          |       |       |  1365 |           |       |       |
| 1   |  SORT AGGREGATE             |          |     1 |    14 |       |           |       |       |
| 2   |   FILTER                    |          |       |       |       |           |       |       |
| 3   |    PARTITION RANGE ITERATOR |          |     1 |    14 |  1365 |  00:00:17 | KEY   | KEY   |
| 4   |     PARTITION LIST SINGLE   |          |     1 |    14 |  1365 |  00:00:17 | 2     | 2     |
| 5   |      TABLE ACCESS FULL      | T_PARTIAL|     1 |    14 |  1365 |  00:00:17 | KEY   | KEY   |
-----------------------------------------------+-----------------------------------+---------------+
Query Block Name / Object Alias (identified by operation id):
------------------------------------------------------------
 1 - SEL$1                
 5 - SEL$1                / "T"@"SEL$1"
------------------------------------------------------------
Predicate Information:
----------------------
2 - filter(:B1>:B2)
5 - filter(("VAL"=3 AND "T"."DT">=:B2 AND "T"."DT"<:B1))
 

Full test case with traces: https://gist.github.com/xtender/e453510c1a23ef6e5ca3beb7af4d30d7

To address these problems, there are two workarounds:

  1. Use the parameter skip_unusable_indexes
  2. Use the hint index

However, both workarounds come with their drawbacks:

  1. it’s not possible to use a hint opt_param('skip_unusable_indexes' 'false'), so you will need to change it on the session level.
  2. In the case of index hint, you may encounter “ORA-01502: index '...' or partition of such index is in unusable state“, if your bind variables switch to partitions with “indexing off“.

So before implementing partial indexing, make sure to test it thoroughly to avoid potential issues with composite partitioning and certain queries, ensuring your database runs smoothly.


Viewing all articles
Browse latest Browse all 16

Latest Images

Trending Articles





Latest Images