Think of it like this, when you
create index t_idx on t(a,b)
it is "as if" it were creating the index on
a || b
if the result of
a || b
is NOT NULL -- an entry will be made in the index structure. If it is NULL, it will not be in the index.
NULL columns may be stored in an index (a b*tree index) however, entirely NULL index entries are NOT stored in the b*tree.
If at least one of the columns in the index is not nullable, we have great opportunities to use this index to find nulls in the table, consider:
ops$tkyte@ORA817DEV.US.ORACLE.COM> create table t ( a int, b int NOT NULL, c int );
Table created.
ops$tkyte@ORA817DEV.US.ORACLE.COM>
ops$tkyte@ORA817DEV.US.ORACLE.COM> insert into t
2 select decode( mod(rownum,100), 0, null, rownum ), rownum, rownum
3 from all_objects;
17148 rows created.
ops$tkyte@ORA817DEV.US.ORACLE.COM>
ops$tkyte@ORA817DEV.US.ORACLE.COM> create index t_idx on t(a,b,c);
Index created.
ops$tkyte@ORA817DEV.US.ORACLE.COM>
ops$tkyte@ORA817DEV.US.ORACLE.COM> analyze table t compute statistics
2 for table
3 for all indexes
4 for all indexed columns;
Table analyzed.
ops$tkyte@ORA817DEV.US.ORACLE.COM>
ops$tkyte@ORA817DEV.US.ORACLE.COM> set autotrace traceonly explain
ops$tkyte@ORA817DEV.US.ORACLE.COM> select a,b from t where a is null;
Execution Plan
----------------------------------------------------------
0 SELECT STATEMENT Optimizer=CHOOSE (Cost=2 Card=171 Bytes=1368)
1 0 INDEX (RANGE SCAN) OF 'T_IDX' (NON-UNIQUE) (Cost=2 Card=171 Bytes=1368)
<b>see how it can use an index range scan now to satisfy IS NULL. Going further, even if B is NULLABLE in this example:</b>
ops$tkyte@ORA817DEV.US.ORACLE.COM>
ops$tkyte@ORA817DEV.US.ORACLE.COM> alter table t modify b null;
Table altered.
<b>under the appropriate cases, we can still use the index to satisfy the is null. Here I just added a predicate on b so that the leading edge of the index is used:
</b>
ops$tkyte@ORA817DEV.US.ORACLE.COM> select a,b from t where a is null and b = 5;
Execution Plan
----------------------------------------------------------
0 SELECT STATEMENT Optimizer=CHOOSE (Cost=2 Card=1 Bytes=8)
1 0 INDEX (RANGE SCAN) OF 'T_IDX' (NON-UNIQUE) (Cost=2 Card=1 Bytes=8)
ops$tkyte@ORA817DEV.US.ORACLE.COM> set autotrace off
<b>and again, we can scan it even though we are looking for NULLs (since the predicate precludes B from being null, we can index range scan safely)</b>