Oracle Scratchpad

December 21, 2023

Descending Bug

Filed under: Bugs,descending indexes,Execution plans,Indexing,Oracle — Jonathan Lewis @ 3:12 pm GMT Dec 21,2023

This is another example of defects in the code to handle descending columns in indexes, with the added feature that the problem is addressed somewhere between 19.4 and 19.10 (it’s present in 19.3, gone in 19.11) – which means that if you upgrade to a recent RU of from some of earlier versions some of your production code may return rows in a different order. On the plus side, it will be the correct order rather than the previously incorrect order. It’s likely that if your code was exhibiting this bug you would have noticed it before the code got to production, so this note is more a confirmation than a realistic warning of a future threat.

The bug originally showed up in a thread on the Oracle developer forum more than a year ago but I was prompted to finish and publish this note after seeing an article on deadlocks by Frank Pachot where the behaviour of his demonstration code could vary with version of Oracle because of this bug.

Here’s some code to create a demonstration data set:

rem
rem     Author:         Jonathan Lewis
rem     Dated:          Aug 2022
rem     Purpose:        
rem
rem     Last tested 
rem             19.11.0.0       Right order
rem             19.3.0.0        Wrong order
rem             12.2.0.1        Wrong order
rem
rem     Notes
rem     From 12.1.0.2 to ca. 19.3(++?) the optimizer loses a "sort order by" 
rem     operation when a "descending" index meets an in-list iterator.
rem     

create table t1 
as
with generator as (
        select  rownum id
        from    dual
        connect by
                level <= 1e4
)
select
        rownum                                  id,
        substr(dbms_random.string('U',6),1,6)   v1,
        rpad('x',100,'x')                       padding
from
        generator
/

alter table t1 modify v1 not null;

update t1 set v1 = 'BRAVO'      where id = 5000;
update t1 set v1 = 'MIKE'       where id = 1000;
update t1 set v1 = 'YANKEE'     where id = 9000;

create index t1_i1 on t1(v1 desc);

I’ve created a table with a column generated as short random strings, then set three rows scattered through that table to specific values, and created an index on that column – but the index is defined with the column descending.

(Reminder: if all the columns in an index are declared as descending that all you’ve done is waste space and introduce an opportunity for the optimizer to go wrong – descending columns in indexes only add value if the index uses a combination of ascending and descending columns).

Here’s a simple query – with the results when executed from SQL*Plus in 12.2.0.1. Note, particularly, the order by clause, the order of the results, and the body of the execution plan:

set serveroutput off

select  v1, id
from    t1
where   v1 in (
                'MIKE',
                'YANKEE',
                'BRAVO'
        ) 
order by 
        v1
;

select * from table(dbms_xplan.display_cursor(format=>'outline'));


V1                               ID
------------------------ ----------
YANKEE                         9000
MIKE                           1000
BRAVO                          5000

3 rows selected.


SQL_ID  6mpvantc0m4ap, child number 0
-------------------------------------
select v1, id from t1 where v1 in (   'MIKE',   'YANKEE',   'BRAVO'  )
order by  v1

Plan hash value: 4226741654

---------------------------------------------------------------------------------------
| Id  | Operation                     | Name  | Rows  | Bytes | Cost (%CPU)| Time     |
---------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT              |       |       |       |    22 (100)|          |
|   1 |  INLIST ITERATOR              |       |       |       |            |          |
|   2 |   TABLE ACCESS BY INDEX ROWID | T1    |     3 |    33 |    22   (0)| 00:00:01 |
|*  3 |    INDEX RANGE SCAN DESCENDING| T1_I1 |    40 |       |     3   (0)| 00:00:01 |
---------------------------------------------------------------------------------------

The most important point, of course, is that the result set is in the wrong order. It’s interesting to note that there is no “sort order by” operation and that the index range scan is described as “descending”. A brief pause for thought suggests that if you do a descending range scan of a “descending index” then the results ought to come out in ascending order which might explain why the optimizer thought it could eliminate the sort operation. However that thought isn’t necessarily valid since the “inlist iterator” means Oracle should be executing “column = constant” once for each value in the list, which would make the ascending/descending nature of the index fairly irrelevant (for this “single-column” example).

When I created the same data set and ran the same query on 19.11.0.0 I got exactly the same execution plan, including matching Predicate Information and Outline Data (apart from the db_version and optimizer_features_enable values, of course), but the result set was in the right order. (It was still wrong in a test against 19.3, so the fix must have appeared somewhere in the 19.4 to 19.11 range.)

Workaround

In this example one of the ways to work around the problem (in 12.2) was to add the index() hint (which is equivalent to the index_rs_asc() hint) to the query, resulting in the following plan (again identical in 12c and 19c):

SQL_ID  6x3ajwf41x91x, child number 0
-------------------------------------
select  /*+ index(t1 t1_i1) */  v1, id from t1 where v1 in (   'MIKE',
 'YANKEE',   'BRAVO'  ) order by  v1

Plan hash value: 1337030419

-----------------------------------------------------------------------------------------------
| Id  | Operation                             | Name  | Rows  | Bytes | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                      |       |       |       |    23 (100)|          |
|   1 |  SORT ORDER BY                        |       |     3 |    33 |    23   (5)| 00:00:01 |
|   2 |   INLIST ITERATOR                     |       |       |       |            |          |
|   3 |    TABLE ACCESS BY INDEX ROWID BATCHED| T1    |     3 |    33 |    22   (0)| 00:00:01 |
|*  4 |     INDEX RANGE SCAN                  | T1_I1 |    40 |       |     3   (0)| 00:00:01 |
-----------------------------------------------------------------------------------------------

The “index range scan” operation is no longer “descending”, and we have a “sort order by” operation. You’ll note that, thanks to blocking sort operation the table access is now “batched”.

Best Guess

The way Oracle handles an IN-list is to start by reducing it to a sorted list of distinct items, before iterating through each item in turn. Then, if there is an order by clause that matches the order of the sorted in-list, and Oracle can walk the index in the right order then it can avoid a “sort order by” operation.

I’m guessing that there may be two separate optimizer strategies in the “descending columns” case that have collided and effectively cancelled each other out:

  • Hypothetical Strategy 1: If there is a “descending index” that can be range scanned for the data the in-list should be sorted in descending order before iterating. (There is a flaw in this suggestion – see below)
  • Hypothetical strategy 2: Because the query has an order by (ascending) clause the index scan should be in descending order to avoid a sort operation.

The flaw in the first suggestion is that the Predicate Information suggests that it’s not true. This is what you get in every case (though the operation number changes to 4 when the plan includes a “sort order by” operation):

Predicate Information (identified by operation id):
---------------------------------------------------
   3 - access(("T1"."SYS_NC00004$"=HEXTORAW('BDADBEA9B0FF') OR
              "T1"."SYS_NC00004$"=HEXTORAW('B2B6B4BAFF') OR
              "T1"."SYS_NC00004$"=HEXTORAW('A6BEB1B4BABAFF')))
       filter((SYS_OP_UNDESCEND("T1"."SYS_NC00004$")='BRAVO' OR
              SYS_OP_UNDESCEND("T1"."SYS_NC00004$")='MIKE' OR
              SYS_OP_UNDESCEND("T1"."SYS_NC00004$")='YANKEE'))

As you can see the values appearing in the access() predicate are the one’s complements of BRAVO, MIKE and YANKEE in that order; in no case was the order reversed, and previous experience says that predicates are used in the order they appear in the Predicate Information.

On the other hand, it’s arguable that the three predicate values should have been reported (in some form) at the inlist iterator operation – so this may be a case where the simplest strategy for presenting the plan doesn’t match the actual activity of the plan.

Post script

If I change the unhinted query to “order by v1 desc” the rows are reported in ascending order in 12.2.0.1, but in the correct descending order in 19.11.

3 Comments »

  1. […] Descending bug (December 2023): Results appearing in the wrong order despite an order by clause when in-lists meet “descending” indexes. […]

    Pingback by Indexing Catalogue | Oracle Scratchpad — December 21, 2023 @ 3:21 pm GMT Dec 21,2023 | Reply

  2. It seems to be the problem still exists in latest RUR (19.22) – here is the results from my ATP instance.


    demo@ATP19C> select v1, id
    2 from t1
    3 where v1 in ('MIKE','YANKEE','BRAVO')
    4 order by v1;

    V1 ID
    ------------------------ ----------
    BRAVO 5000
    MIKE 1000
    YANKEE 9000

    demo@ATP19C>
    demo@ATP19C> select * from table(dbms_xplan.display_cursor(format=>'allstats last'));

    PLAN_TABLE_OUTPUT
    ----------------------------------------------------------------------------------------------------
    SQL_ID 1zcawhvkudxjx, child number 0
    -------------------------------------
    select v1, id from t1 where v1 in ('MIKE','YANKEE','BRAVO') order
    by v1

    Plan hash value: 4226741654

    -------------------------------------------------------------------------------------------------
    | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
    -------------------------------------------------------------------------------------------------
    | 0 | SELECT STATEMENT | | 1 | | 3 |00:00:00.01 | 9 |
    | 1 | INLIST ITERATOR | | 1 | | 3 |00:00:00.01 | 9 |
    | 2 | TABLE ACCESS BY INDEX ROWID | T1 | 3 | 3 | 3 |00:00:00.01 | 9 |
    |* 3 | INDEX RANGE SCAN DESCENDING| T1_I1 | 3 | 40 | 3 |00:00:00.01 | 6 |
    -------------------------------------------------------------------------------------------------

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

    3 - access(("T1"."SYS_NC00004$"=HEXTORAW('BDADBEA9B0FF') OR
    "T1"."SYS_NC00004$"=HEXTORAW('B2B6B4BAFF') OR
    "T1"."SYS_NC00004$"=HEXTORAW('A6BEB1B4BABAFF')))
    filter((SYS_OP_UNDESCEND("T1"."SYS_NC00004$")='BRAVO' OR
    SYS_OP_UNDESCEND("T1"."SYS_NC00004$")='MIKE' OR
    SYS_OP_UNDESCEND("T1"."SYS_NC00004$")='YANKEE'))

    26 rows selected.

    demo@ATP19C> select /*+ index(t1,t1_i1) */ v1, id
    2 from t1
    3 where v1 in ('MIKE','YANKEE','BRAVO')
    4 order by v1;

    V1 ID
    ------------------------ ----------
    BRAVO 5000
    MIKE 1000
    YANKEE 9000

    demo@ATP19C>
    demo@ATP19C> select * from table(dbms_xplan.display_cursor(format=>'allstats last'));

    PLAN_TABLE_OUTPUT
    -------------------------------------------------------------------------------------------------------------------
    SQL_ID c613akv50b32k, child number 0
    -------------------------------------
    select /*+ index(t1,t1_i1) */ v1, id from t1 where v1 in
    ('MIKE','YANKEE','BRAVO') order by v1

    Plan hash value: 1337030419

    ------------------------------------------------------------------------------------------------------------------------------------
    | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem |
    ------------------------------------------------------------------------------------------------------------------------------------
    | 0 | SELECT STATEMENT | | 1 | | 3 |00:00:00.01 | 8 | | | |
    | 1 | SORT ORDER BY | | 1 | 3 | 3 |00:00:00.01 | 8 | 2048 | 2048 | 2048 (0)|
    | 2 | INLIST ITERATOR | | 1 | | 3 |00:00:00.01 | 8 | | | |
    | 3 | TABLE ACCESS BY INDEX ROWID BATCHED| T1 | 3 | 3 | 3 |00:00:00.01 | 8 | | | |
    |* 4 | INDEX RANGE SCAN | T1_I1 | 3 | 40 | 3 |00:00:00.01 | 5 | 1028K| 1028K| |
    ------------------------------------------------------------------------------------------------------------------------------------

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

    4 - access(("T1"."SYS_NC00004$"=HEXTORAW('BDADBEA9B0FF') OR "T1"."SYS_NC00004$"=HEXTORAW('B2B6B4BAFF') OR
    "T1"."SYS_NC00004$"=HEXTORAW('A6BEB1B4BABAFF')))
    filter((SYS_OP_UNDESCEND("T1"."SYS_NC00004$")='BRAVO' OR SYS_OP_UNDESCEND("T1"."SYS_NC00004$")='MIKE' OR
    SYS_OP_UNDESCEND("T1"."SYS_NC00004$")='YANKEE'))

    25 rows selected.

    demo@ATP19C> select banner_full from v$version;

    BANNER_FULL
    ----------------------------------------------------------------------------------------------------------------------------------------------------------------
    Oracle Database 19c Enterprise Edition Release 19.0.0.0.0 - Production
    Version 19.22.0.1.0

    demo@ATP19C>

    Comment by Rajeshwaran Jeyabal — December 22, 2023 @ 2:03 am GMT Dec 22,2023 | Reply


RSS feed for comments on this post. TrackBack URI

Comments and related questions are welcome.

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Website Powered by WordPress.com.