Oracle SQL
  • LICENSE

Slow index access “COL=:N” where :N is NULL

Posted on October 31, 2022 by Sayan Malakshinov Posted in CBO, curious, Funny, oracle, query optimizing, SQL, troubleshooting 2,364 Page views

All Oracle specialists know that a predicate X=NULL can never be true and we should use “X is NULL” in such cases. The Oracle optimizer knows about that, so if we create a table like this:

create table tnulls
  as 
    select
      level as N
     ,case when mod(level,10)=1 then level
           else null
      end as A
    from dual
    connect by level<=1000000;
create index ix_a on tnulls(a desc);
create index ix_an on tnulls(A, N);

In case of simple A=NULL:

select 1 from tnulls where A=null

Plan hash value: 3333700597

--------------------------------------------------------------------
| Id  | Operation          | Name   | Starts | E-Rows | Cost (%CPU)|
--------------------------------------------------------------------
|   0 | SELECT STATEMENT   |        |      1 |        |     1 (100)|
|*  1 |  FILTER            |        |      1 |        |            |
|   2 |   TABLE ACCESS FULL| TNULLS |      0 |   1000K|   444   (2)|
--------------------------------------------------------------------

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

   1 - filter(NULL IS NOT NULL)

CBO adds filter(NULL IS NOT NULL), so it doesn’t even start the child operation TABLE ACCESS FULL. And even in the case of using a NULL bind variable – “A=:varnull” with FTS or IRS, we can see that Oracle doesn’t read data blocks:

create index ix_an on tnulls(A, N);
alter session set statistics_level=all;
SQL> select/*+ no_index(tnulls) */ 2 from tnulls where A=:varnull;

no rows selected

SQL> select * from table(dbms_xplan.display_cursor('','','typical allstats last'));

PLAN_TABLE_OUTPUT
-----------------------------------------------------------------------------------
SQL_ID  4xf5hr7mm4zvz, child number 0
-------------------------------------
select/*+ no_index(tnulls) */ 2 from tnulls where A=:varnull

Plan hash value: 3133871912

------------------------------------------------------------------------------------------------------------
| Id  | Operation         | Name   | Starts | E-Rows |E-Bytes| Cost (%CPU)| E-Time   | A-Rows |   A-Time   |
------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT  |        |      1 |        |       |   446 (100)|          |      0 |00:00:00.01 |
|*  1 |  TABLE ACCESS FULL| TNULLS |      1 |      1 |     2 |   446   (2)| 00:00:01 |      0 |00:00:00.01 |
------------------------------------------------------------------------------------------------------------

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

   1 - filter("A"=:VARNULL)
SQL> select/*+ index(tnulls ix_an) */ 3 from tnulls where A=:varnull;

no rows selected

SQL> select * from table(dbms_xplan.display_cursor('','','typical allstats last'));

PLAN_TABLE_OUTPUT
--------------------------------------------------------------------------------------------
SQL_ID  b5qz9yv13tyr8, child number 0
-------------------------------------
select/*+ index(tnulls ix_an) */ 3 from tnulls where A=:varnull

Plan hash value: 404583077

----------------------------------------------------------------------------------------------------------
| Id  | Operation        | Name  | Starts | E-Rows |E-Bytes| Cost (%CPU)| E-Time   | A-Rows |   A-Time   |
----------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT |       |      1 |        |       |     2 (100)|          |      0 |00:00:00.01 |
|*  1 |  INDEX RANGE SCAN| IX_AN |      1 |      1 |     2 |     2   (0)| 00:00:01 |      0 |00:00:00.01 |
----------------------------------------------------------------------------------------------------------

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

   1 - access("A"=:VARNULL)

But take a look what happens if we create a descending index: index ix_a_desc on tnulls(a desc):

SQL> create index ix_a_desc on tnulls(a desc);
SQL> select/*+ index(tnulls ix_a_desc) */ 4 from tnulls where A=:varnull;

no rows selected

SQL> select * from table(dbms_xplan.display_cursor('','','typical allstats last'));

PLAN_TABLE_OUTPUT
----------------------------------------------------------------------------------------
SQL_ID  8n09rudu2pr4j, child number 0
-------------------------------------
select/*+ index(tnulls ix_a_desc) */ 4 from tnulls where A=:varnull

Plan hash value: 3186386750

----------------------------------------------------------------------------------------
| Id  | Operation        | Name      | Starts | A-Rows |   A-Time   | Buffers | Reads  |
----------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT |           |      1 |      0 |00:00:00.06 |    1633 |   1632 |
|*  1 |  INDEX RANGE SCAN| IX_A_DESC |      1 |      0 |00:00:00.06 |    1633 |   1632 |
----------------------------------------------------------------------------------------

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

   1 - access("TNULLS"."SYS_NC00003$"=SYS_OP_DESCEND(:VARNULL))
       filter(SYS_OP_UNDESCEND("TNULLS"."SYS_NC00003$")=:VARNULL)

Many of us know that descending indexes are FBIs (function-based index), but it appears not everyone knows that SYS_OP_DESCEND(NULL) returns 0x00 (RAW data type), so such indexes contain NULLs. This is why IRS scans all those index blocks by access predicates and filters out all satisfied rows by FILTER predicate (SYS_OP_UNDESCEND(VirtCol) = :VARNULL).

Originally, I got a question with a plan and statistics from tkprof output:

SELECT/*+ index(T IDX_AB) */ * FROM T where A=:B1 and B=:B2;

We have an index on T(A,B), where each pair of (A,B) is nearly unique, so why is index access using that index so slow?

Upon examining this, I immediately noticed SYS_OP_DESCEND/SYS_OP_UNDESCEND in the predicate section and requested a raw trace to verify the bind variables. Once I received it, I discovered that over 90% of them were, in fact, NULLs. Since they don’t really need to return any rows in case of NULLs, the issue was solved with a simple rewrite:

SELECT/*+ index(T IDX_AB) */ * FROM T where A=:B1 and B=:B2;

to

SELECT/*+ index(T IDX_AB) */ * FROM T where A=:B1 and B=:B2
 and :B1 is not null and :B2 is not null;

In conclusion, even seemingly simple aspects like predicates with NULLs can have significant nuances when dealing with unusual scenarios, such as descending indexes. Being aware of these subtleties can help you better understand the inner workings of Oracle indexes and optimize query performance in more complex situations.

cbo oracle query optimization troubleshooting
« Where does the commit or rollback happen in PL/SQL code?
CBO and Partial indexing »
Page views: 2,364
photo Sayan Malakshinov

Oracle ACE Pro Oracle ACE Pro Alumni

DEVVYOracle Database Developer Choice Award winner

Oracle performance tuning expert

UK / Cambridge

LinkedIn   Twitter
sayan@orasql.org

Recent Posts

  • Oracle Telegram Bot
  • Partition Pruning and Global Indexes
  • Interval Search: Part 4. Dynamic Range Segmentation – interval quantization
  • Interval Search Series: Simplified, Advanced, and Custom Solutions
  • Interval Search: Part 3. Dynamic Range Segmentation – Custom Domain Index

Popular posts

Recent Comments

  • Oracle SQL | Interval Search: Part 4. Dynamic Range Segmentation – interval quantization on Interval Search: Part 3. Dynamic Range Segmentation – Custom Domain Index
  • Oracle SQL | Interval Search: Part 4. Dynamic Range Segmentation – interval quantization on Interval Search: Part 2. Dynamic Range Segmentation – Simplified
  • Oracle SQL | Interval Search: Part 4. Dynamic Range Segmentation – interval quantization on Interval Search: Optimizing Date Range Queries – Part 1
  • Oracle SQL | Interval Search Series: Simplified, Advanced, and Custom Solutions on Interval Search: Part 2. Dynamic Range Segmentation – Simplified
  • Oracle SQL | Interval Search: Part 2. Dynamic Range Segmentation – Simplified on Interval Search: Part 3. Dynamic Range Segmentation – Custom Domain Index

Blogroll

  • Alex Fatkulin
  • Alexander Anokhin
  • Andrey Nikolaev
  • Charles Hooper
  • Christian Antognini
  • Coskan Gundogar
  • David Fitzjarrell
  • Igor Usoltsev
  • Jonathan Lewis
  • Karl Arao
  • Mark Bobak
  • Martin Bach
  • Martin Berger
  • Neil Chandler
  • Randolf Geist
  • Richard Foote
  • Riyaj Shamsudeen
  • Tanel Poder
  • Timur Akhmadeev
  • Valentin Nikotin
©Sayan Malakshinov. Oracle SQL