Monday, December 22, 2014

Local prefixed vs. nonprefixed indexes: Column Null Values

According to Oracle Doc, Oracle does not index table rows in which all key columns are null except in the case of bitmap indexes (Database SQL Language Reference).

Therefore, in a composite B-tree index, if there exists one column which contains a non-null value, the row will be indexed. Since local prefixed indexes is a composite index which includes partition keys on the leading edge of the index definition, and partition keys are never all null, all rows are always indexed.

For applications, it means that local prefixed indexes is always usable, but not local nonprefixed indexes. In the performance point of view, Column Null Values is a torque on Local prefixed vs. nonprefixed indexes (Number_of_Null_Columns x Number_of_Rows).

Taking the same example from Expert Oracle Database Architecture Page 593, but updating Column b of one row in PART_1 as NULL.

 create table partitioned_table
 ( a int,
   b int,
   data char(20)
 )
 partition by range (a)
 (
 partition part_1 values less than(2) tablespace p1,
 partition part_2  values less than(3) tablespace p2
 )
 /

 create index local_prefixed on partitioned_table (a,b) local;

 create index local_nonprefixed on partitioned_table (b) local;

 insert into partitioned_table
  select mod(rownum-1,2)+1, rownum, 'x' from all_objects where rownum <= 1000;

 update partitioned_table set b=null where a=1 and b=99;
  
 begin
    dbms_stats.gather_table_stats
    ( user,
     'PARTITIONED_TABLE',
      cascade=>TRUE );
 end;
 /


we can see PART_1 having one row with Column b being NULL:

 select a, count(*), count(a), count(b)
  from partitioned_table group by a;

           A   COUNT(*)   COUNT(A)   COUNT(B)
  ---------- ---------- ---------- ----------
           1        500        500        499
           2        500        500        500

 select index_name, partition_name, num_rows
   from dba_ind_partitions
  where index_name in ('LOCAL_PREFIXED', 'LOCAL_NONPREFIXED');

 INDEX_NAME                     PARTITION_NAME                   NUM_ROWS
 ------------------------------ ------------------------------ ----------
 LOCAL_NONPREFIXED              PART_1                                499
 LOCAL_NONPREFIXED              PART_2                                500
 LOCAL_PREFIXED                 PART_1                                500
 LOCAL_PREFIXED                 PART_2                                500


Now if we run following queries, all of them will use LOCAL_PREFIXED index.

 select /*+ index(t local_prefixed) */ count(a) from partitioned_table t where a = 1 and b is null;

 select /*+ index(t local_nonprefixed) */ count(a) from partitioned_table t where a = 1 and b is null;

 select /*+ index(t local_prefixed) */ count(a) from partitioned_table t where b is null;

 select /*+ index(t local_nonprefixed) */ count(a) from partitioned_table t where b is null;

 select /*+ index(t local_prefixed) */ count(a) from partitioned_table t where a is null and b is null;

 select /*+ index(t local_nonprefixed) */ count(a)

   from partitioned_table t where a is null and b is null;

  -----------------------------------------------------------------------------------------------
  | Id  | Operation               | Name           | Rows  | Bytes | Cost (%CPU)| Pstart| Pstop |
  -----------------------------------------------------------------------------------------------
  |   0 | SELECT STATEMENT        |                |     1 |     7 |     1   (0)|       |       |
  |   1 |  SORT AGGREGATE         |                |     1 |     7 |            |       |       |
  |   2 |   PARTITION RANGE SINGLE|                |     1 |     7 |     1   (0)|     1 |     1 |
  |*  3 |    INDEX RANGE SCAN     | LOCAL_PREFIXED |     1 |     7 |     1   (0)|     1 |     1 |
  -----------------------------------------------------------------------------------------------

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

     3 - access("A"=1 AND "B" IS NULL)


For the queries with partition key being NULL, PARTITION RANGE EMPTY elimination is used.

Even though PART_2 does not contain any rows whose Column b being NULL, LOCAL_NONPREFIXED index can neither be used in the following queries.

 select /*+ index(t local_prefixed) */ count(a) from partitioned_table t where a = 2 and b is null;

 select /*+ index(t local_nonprefixed) */ count(a) from partitioned_table t where a = 2 and b is null;

 select /*+ index(t local_nonprefixed) */ count(a)

   from partitioned_table partition (part_2) t where b is null;