It is important to note that the issue is not present in 19c - I have reproduced it only on 12.2.0.1.190416 and 18c.
The listings that I am using in this article are taken from 18c.
SQL> @or_expand_setup SQL> drop table t1; Table dropped. SQL> SQL> create table t1 ( 2 part_key varchar2(8), 3 status varchar2(12), 4 pad1 char(500) 5 ) 6 partition by list(part_key) ( 7 partition values ('P1'), 8 partition values (default) 9 ) 10 ; Table created. SQL> SQL> SQL> insert into t1 2 select 'P1', status, 'X' 3 from (select 90 pct, 'PROCESSED' status from dual union all 4 select 1, 'UNPROCESSED' from dual union all 5 select 9, 'PENDING' from dual) params, 6 lateral( 7 select level 8 from dual 9 connect by level <= params.pct * 1000 10 ) duplicator; 100000 rows created. SQL> SQL> commit; Commit complete. SQL> SQL> create index t1_status_i on t1(status) local; Index created. SQL> SQL> select status, 2 count(*), 3 round(ratio_to_report(count(*)) over () * 100, 2) pct 4 from t1 5 group by status 6 order by 1; STATUS COUNT(*) PCT ------------ ---------- ---------- PENDING 9000 9 PROCESSED 90000 90 UNPROCESSED 1000 1In a nutshell, it is a list-partitioned table with a local index. 90% of the rows are in the
PROCESSED
status, whereas PENDING
and UNPROCESSED
statuses account only for 9% and 1% correspondingly.The script itself: or_expand.sql
Next, I am going to run a simple query and check its plan with and without a virtual column on the
STATUS
column:select --+ gather_plan_statistics or_expand(@sel$1 (1) (2)) count(pad1) from t1 where part_key = :part_key and (:param = 'WAITING' and status = 'UNPROCESSED' or :param = 'ALL' and status <> 'PENDING');The variables PARAM and PART_KEY are set as follows:
var part_key varchar2(10)='P1' var param varchar2(12)='WAITING'Thus, I am going to query a single partition to get the rows in the
UNPROCESSED
status from it.Here is the result of the script without the virtual column first:
SQL> @or_expand_test_no_vcol SQL> set def on lin 124 pages 100 SQL> SQL> -- alter table t1 add lower_status varchar2(128) generated always as (lower(status)) virtual; SQL> SQL> exec dbms_stats.gather_table_stats( '', 't1', method_opt => 'for all columns size 254') PL/SQL procedure successfully completed. SQL> SQL> col tfi old_v tfi nopri SQL> SQL> select to_char(sysdate, 'yyyymmdd_hh24miss') tfi from dual; SQL> SQL> alter session set tracefile_identifier='&tfi.'; old 1: alter session set tracefile_identifier='&tfi.' new 1: alter session set tracefile_identifier='20200520_181304' Session altered. SQL> SQL> alter session set events 'trace[sql_optimizer.*]'; Session altered. SQL> SQL> SQL> var part_key varchar2(10)='P1' SQL> var param varchar2(12)='WAITING' SQL> SQL> select --+ gather_plan_statistics or_expand(@sel$1 (1) (2)) 2 count(pad1) 3 from t1 4 where part_key = :part_key 5 and (:param = 'WAITING' and status = 'UNPROCESSED' 6 or 7 :param = 'ALL' and status <> 'PENDING'); COUNT(PAD1) ----------- 1000 SQL> SQL> select * 2 from dbms_xplan.display_cursor(format=> 'allstats last outline'); PLAN_TABLE_OUTPUT ---------------------------------------------------------------------------------------------------------------------------- SQL_ID fq8b3jumk131t, child number 0 ------------------------------------- An uncaught error happened in prepare_sql_statement : ORA-01403: no data found Plan hash value: 1293629841 ----------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | ----------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.02 | 7831 | | 1 | SORT AGGREGATE | | 1 | 1 | 1 |00:00:00.02 | 7831 | | 2 | PARTITION LIST SINGLE| | 1 | 920 | 1000 |00:00:00.01 | 7831 | |* 3 | TABLE ACCESS FULL | T1 | 1 | 920 | 1000 |00:00:00.01 | 7831 | ----------------------------------------------------------------------------------------- Outline Data ------------- /*+ BEGIN_OUTLINE_DATA IGNORE_OPTIM_EMBEDDED_HINTS OPTIMIZER_FEATURES_ENABLE('18.1.0') DB_VERSION('18.1.0') ALL_ROWS OUTLINE_LEAF(@"SEL$1") FULL(@"SEL$1" "T1"@"SEL$1") END_OUTLINE_DATA */ Predicate Information (identified by operation id): --------------------------------------------------- 3 - filter((((:PARAM='ALL' AND "STATUS"<>'PENDING') OR ("STATUS"='UNPROCESSED' AND :PARAM='WAITING')) AND "PART_KEY"=:PART_KEY)) 35 rows selected. SQL> SQL> alter session set events 'trace[sql_optimizer.*] off'; Session altered. SQL> SQL> col value for a80 SQL> SQL> select value 2 from v$diag_info 3 where name='Default Trace File'; VALUE -------------------------------------------------------------------------------- /u01/app/oracle/diag/rdbms/orcl/orcl/trace/orcl_ora_2137_20200520_181304.trc
I highlighted the commented ALTER TABLE ADD COLUMN command. I am going to refer to it below.
It is worth noting that the OR Expansion transformation has not been performed and the relevant trace file contains a clue:
*********************************** Cost-Based OR Expansion *********************************** ORE: Trying CBQT OR expansion before unnesting ORE: Checking validity of OR Expansion for query block SEL$1 (#1) ORE: Predicate chain before QB validity check - SEL$1 "T1"."PART_KEY"=:B1 AND (:B2='WAITING' AND "T1"."STATUS"='UNPROCESSED' OR :B3='ALL' AND "T1"."STATUS"<>'PENDING') ORE: Predicate chain after QB validity check - SEL$1 "T1"."PART_KEY"=:B1 AND (:B2='WAITING' AND "T1"."STATUS"='UNPROCESSED' OR :B3='ALL' AND "T1"."STATUS"<>'PENDING') OR Expansion on query block SEL$1 (#1) ORE: Checking validity of OR Expansion for query block SEL$1 (#1) ORE: Predicate chain before QB validity check - SEL$1 "T1"."PART_KEY"=:B1 AND (:B2='WAITING' AND "T1"."STATUS"='UNPROCESSED' OR :B3='ALL' AND "T1"."STATUS"<>'PENDING') ORE: Predicate chain after QB validity check - SEL$1 "T1"."PART_KEY"=:B1 AND (:B2='WAITING' AND "T1"."STATUS"='UNPROCESSED' OR :B3='ALL' AND "T1"."STATUS"<>'PENDING') ORE: Using search type: linear ORE: Checking validity of OR Expansion for query block SEL$1 (#1) ORE: Predicate chain before QB validity check - SEL$1 "T1"."PART_KEY"=:B1 AND (:B2='WAITING' AND "T1"."STATUS"='UNPROCESSED' OR :B3='ALL' AND "T1"."STATUS"<>'PENDING') ORE: Predicate chain after QB validity check - SEL$1 "T1"."PART_KEY"=:B1 AND (:B2='WAITING' AND "T1"."STATUS"='UNPROCESSED' OR :B3='ALL' AND "T1"."STATUS"<>'PENDING') ORE: Checking validity of disjunct chain ORE: Bypassed for disjunct chain: No Index or Partition driver found. ORE: # conjunction chain - 1 ORE: No state generated.OFF: I came across that
ORE: Bypassed for disjunct chain: No Index or Partition driver found.
message in one of my previous posts: Concatenation and filter subqueries oddity.What can go wrong if we now uncomment the aforementioned ALTER TABLE ADD COLUMN command?
The script or_expand_test_vcol.sql:
SQL> @or_expand_test_vcol SQL> set def on lin 124 pages 100 SQL> SQL> alter table t1 add lower_status varchar2(128) generated always as (lower(status)) virtual; Table altered. SQL> SQL> exec dbms_stats.gather_table_stats( '', 't1', method_opt => 'for all columns size 254') PL/SQL procedure successfully completed. SQL> SQL> col tfi old_v tfi nopri SQL> SQL> select to_char(sysdate, 'yyyymmdd_hh24miss') tfi from dual; SQL> SQL> alter session set tracefile_identifier='&tfi.'; old 1: alter session set tracefile_identifier='&tfi.' new 1: alter session set tracefile_identifier='20200520_181319' Session altered. SQL> SQL> alter session set events 'trace[sql_optimizer.*]'; Session altered. SQL> SQL> SQL> var part_key varchar2(10)='P1' SQL> var param varchar2(12)='WAITING' SQL> SQL> select --+ gather_plan_statistics or_expand(@sel$1 (1) (2)) 2 count(pad1) 3 from t1 4 where part_key = :part_key 5 and (:param = 'WAITING' and status = 'UNPROCESSED' 6 or 7 :param = 'ALL' and status <> 'PENDING'); COUNT(PAD1) ----------- 1000 SQL> SQL> select * 2 from dbms_xplan.display_cursor(format=> 'allstats last outline'); PLAN_TABLE_OUTPUT ---------------------------------------------------------------------------------------------------------------------------- SQL_ID fq8b3jumk131t, child number 0 ------------------------------------- An uncaught error happened in prepare_sql_statement : ORA-01403: no data found Plan hash value: 3973059565 ---------------------------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | ---------------------------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.01 | 83 | | 1 | SORT AGGREGATE | | 1 | 1 | 1 |00:00:00.01 | 83 | | 2 | VIEW | VW_ORE_BA8ECEFB | 1 | 91982 | 1000 |00:00:00.01 | 83 | | 3 | UNION-ALL | | 1 | | 1000 |00:00:00.01 | 83 | |* 4 | FILTER | | 1 | | 1000 |00:00:00.01 | 83 | | 5 | PARTITION LIST SINGLE | | 1 | 999 | 1000 |00:00:00.01 | 83 | |* 6 | TABLE ACCESS BY LOCAL INDEX ROWID BATCHED| T1 | 1 | 999 | 1000 |00:00:00.01 | 83 | |* 7 | INDEX RANGE SCAN | T1_STATUS_I | 1 | 1000 | 1000 |00:00:00.01 | 6 | |* 8 | FILTER | | 1 | | 0 |00:00:00.01 | 0 | | 9 | PARTITION LIST SINGLE | | 0 | 90983 | 0 |00:00:00.01 | 0 | |* 10 | TABLE ACCESS FULL | T1 | 0 | 90983 | 0 |00:00:00.01 | 0 | ---------------------------------------------------------------------------------------------------------------------------- Outline Data ------------- /*+ BEGIN_OUTLINE_DATA IGNORE_OPTIM_EMBEDDED_HINTS OPTIMIZER_FEATURES_ENABLE('18.1.0') DB_VERSION('18.1.0') ALL_ROWS OUTLINE_LEAF(@"SET$9162BF3C_2") OUTLINE_LEAF(@"SET$9162BF3C_1") OUTLINE_LEAF(@"SET$9162BF3C") OR_EXPAND(@"SEL$1" (1) (2)) OUTLINE_LEAF(@"SEL$BA8ECEFB") OUTLINE(@"SEL$1") NO_ACCESS(@"SEL$BA8ECEFB" "VW_ORE_BA8ECEFB"@"SEL$BA8ECEFB") INDEX_RS_ASC(@"SET$9162BF3C_1" "T1"@"SET$9162BF3C_1" ("T1"."STATUS")) BATCH_TABLE_ACCESS_BY_ROWID(@"SET$9162BF3C_1" "T1"@"SET$9162BF3C_1") FULL(@"SET$9162BF3C_2" "T1"@"SET$9162BF3C_2") END_OUTLINE_DATA */ Predicate Information (identified by operation id): --------------------------------------------------- 4 - filter(:PARAM='WAITING') 6 - filter("PART_KEY"=:PART_KEY) 7 - access("STATUS"='UNPROCESSED') 8 - filter(:PARAM='ALL') 10 - filter(("STATUS"<>'PENDING' AND "PART_KEY"=:PART_KEY AND (LNNVL(:PARAM='WAITING') OR LNNVL("STATUS"='UNPROCESSED')))) 54 rows selected. SQL> SQL> alter session set events 'trace[sql_optimizer.*] off'; Session altered. SQL> SQL> col value for a80 SQL> SQL> select value 2 from v$diag_info 3 where name='Default Trace File'; VALUE -------------------------------------------------------------------------------- /u01/app/oracle/diag/rdbms/orcl/orcl/trace/orcl_ora_2137_20200520_181319.trc
The relevant section of the trace file is as follows:
*********************************** Cost-Based OR Expansion *********************************** ORE: Trying CBQT OR expansion before unnesting ORE: Checking validity of OR Expansion for query block SEL$1 (#1) ORE: Predicate chain before QB validity check - SEL$1 "T1"."PART_KEY"=:B1 AND (:B2='WAITING' AND "T1"."STATUS"='UNPROCESSED' OR :B3='ALL' AND "T1"."STATUS"<>'PENDING') ORE: Predicate chain after QB validity check - SEL$1 "T1"."PART_KEY"=:B1 AND (:B2='WAITING' AND "T1"."STATUS"='UNPROCESSED' OR :B3='ALL' AND "T1"."STATUS"<>'PENDING') OR Expansion on query block SEL$1 (#1) ORE: Checking validity of OR Expansion for query block SEL$1 (#1) ORE: Predicate chain before QB validity check - SEL$1 "T1"."PART_KEY"=:B1 AND (:B2='WAITING' AND "T1"."STATUS"='UNPROCESSED' OR :B3='ALL' AND "T1"."STATUS"<>'PENDING') ORE: Predicate chain after QB validity check - SEL$1 "T1"."PART_KEY"=:B1 AND (:B2='WAITING' AND "T1"."STATUS"='UNPROCESSED' OR :B3='ALL' AND "T1"."STATUS"<>'PENDING') ORE: Using search type: linear ORE: Checking validity of OR Expansion for query block SEL$1 (#1) ORE: Predicate chain before QB validity check - SEL$1 "T1"."PART_KEY"=:B1 AND (:B2='WAITING' AND "T1"."STATUS"='UNPROCESSED' OR :B3='ALL' AND "T1"."STATUS"<>'PENDING') ORE: Predicate chain after QB validity check - SEL$1 "T1"."PART_KEY"=:B1 AND (:B2='WAITING' AND "T1"."STATUS"='UNPROCESSED' OR :B3='ALL' AND "T1"."STATUS"<>'PENDING') ORE: Checking validity of disjunct chain ORE: # conjunction chain - 2 ORE: Checking validity of disjunct chain ORE: Predicate list P1 : "T1"."PART_KEY"=:B1 P2 : :B1='WAITING' P3 : "T1"."STATUS"='UNPROCESSED' P4 : :B1='ALL' P5 : "T1"."STATUS"<>'PENDING' DNF Matrix (Before sorting OR branches) P1 P2 P3 P4 P5 CNJ (#1) : 1 1 1 0 0 CNJ (#2) : 1 0 0 1 1I have made the following observations regarding this issue:
- The virtual column should be on the STATUS column. Neither PART_KEY, nor PAD1 works - they both result in the absence of OR Expansion.
- The OR Expansion transformation kicks in if there are extended statistics on the STATUS column, such as: SYS_OP_COMBINED_HASH(STATUS, PART_KEY).
- Unsurprisingly, there is OR Expansion when there is an FBI, such as:
create index t1_lower_status_i on t1(lower(status)) local;
- The full outline of the query with OR Expansion does not lead to the OR Expansion transformation in the examples from this article where there was no OR Expansion.
- Once again, 19c behaves differently - there is no OR Expansion in the given examples when 12.2 and 18c performs it. I am going to talk about in the next blog post.
Although I do expect OR Expansion in the queries from this article, I am not able to make a connection between OR Expansion and virtual columns/extended statistics/FBIs in the provided examples.
Hence, this issue is a bit mixed bag for me: I would love to have OR Expansion - the examples were designed after real application queries after all (they benefit from OR Expansion) - it might as well be a bug that was fixed in 19c.
Time will tell. I have raised an SR to follow up on this issue: SR 3-22449907431 : OR-Expansion does not work following 19.5 upgrade. Hopefully, Oracle Support will explain it one day - we have not made any progress during last 2 months which has become customary while working with Oracle Support.