Towards the end of last year I used a query with a couple of “constant” subqueries as a focal point for a blog note on reading parallel execution plans. One of the comments on that note raised a question about cardinality estimates and, coincidentally, I received an email about the cost calculations for a similar query a few days later.
Unfortunately there are all sorts of anomalies, special cases, and changes that show up across versions when subqueries come into play – it’s only in recent versions of 11.2, for example, that a very simple example I’ve got of three equivalent statements that produce the same execution plan report the same costs and cardinality. (The queries are: table with IN subquery, table with EXISTS subquery, table joined to “manually unnested” subquery – the three plans take the unnested subquery shape.)
I’m just going to pick out one particular anomaly, which is a costing error with multiple subqueries when “OR-ed”. Here’s my sample data set:
rem rem Script: subq_cost_anomaly_2.sql rem Author: Jonathan Lewis rem Dated: Jan 2016 rem rem Last tested rem 12.2.0.1 rem 12.1.0.2 rem 11.2.0.4 create table t1 nologging as select rownum n1, rownum n2, rownum n3, lpad(rownum,10) small_vc, rpad('x',100,'x') padding from dual connect by level <= 20000 -- > comment to avoid wordpress format issue ; create table t2 nologging as select rownum n1, rownum n2, rownum n3, lpad(rownum,10) small_vc, rpad('x',100,'x') padding from dual connect by level <= 25000 -- > comment to avoid wordpress format issue ; create table t3 nologging as select rownum n1, rownum n2, rownum n3, lpad(rownum,10) small_vc, rpad('x',100,'x') padding from dual connect by level <= 30000 -- > comment to avoid wordpress format issue ; begin dbms_stats.gather_table_stats( ownname => user, tabname =>'t1', method_opt => 'for all columns size 1' ); dbms_stats.gather_table_stats( ownname => user, tabname =>'t2', method_opt => 'for all columns size 1' ); dbms_stats.gather_table_stats( ownname => user, tabname =>'t3', method_opt => 'for all columns size 1' ); end; /
The three tables are slightly different sizes so that it will be easy to see different costs of tablescans and there are no indexes so everything I do in the queries will be tablescans. Here are six queries I’m going to test – they all scan t1, with “constant” subqueries against t2 and/or t3. The first pair is just to show you the basic cost of the query with a single subquery, the second pair shows you the default action with two subqueries in two different orders, the final pair shows you what happens with two subqueries when you block “subquery pushing”.
select max(t1.n1) from t1 where t1.n1 > 10000 and t1.n2 > (select avg(t2.n2) from t2) ; select max(t1.n1) from t1 where t1.n1 > 10000 and t1.n3 > (select avg(t3.n3) from t3) ; select max(t1.n1) from t1 where t1.n1 > 10000 and ( t1.n2 > (select avg(t2.n2) from t2) or t1.n3 > (select avg(t3.n3) from t3) ) ; select max(t1.n1) from t1 where t1.n1 > 10000 and ( t1.n3 > (select avg(t3.n3) from t3) or t1.n2 > (select avg(t2.n2) from t2) ) ; select max(t1.n1) from t1 where t1.n1 > 10000 and ( t1.n2 > (select /*+ no_push_subq */ avg(t2.n2) from t2) or t1.n3 > (select /*+ no_push_subq */ avg(t3.n3) from t3) ) ; select max(t1.n1) from t1 where t1.n1 > 10000 and ( t1.n3 > (select /*+ no_push_subq */ avg(t3.n3) from t3) or t1.n2 > (select /*+ no_push_subq */ avg(t2.n2) from t2) ) ;
Here are the first two plans, pulled from memory (which you might have guessed thanks to the “disappearing subquery” in the Predicate Information section. These examples came from 12.1.0.2, but the same happens in 11.2.0.4:
----------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | ----------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | | | 111 (100)| | | 1 | SORT AGGREGATE | | 1 | 10 | | | |* 2 | TABLE ACCESS FULL | T1 | 500 | 5000 | 49 (3)| 00:00:01 | | 3 | SORT AGGREGATE | | 1 | 5 | | | | 4 | TABLE ACCESS FULL| T2 | 25000 | 122K| 62 (4)| 00:00:01 | ----------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 2 - filter(("T1"."N1">10000 AND "T1"."N2">)) ----------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | ----------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | | | 123 (100)| | | 1 | SORT AGGREGATE | | 1 | 10 | | | |* 2 | TABLE ACCESS FULL | T1 | 500 | 5000 | 49 (3)| 00:00:01 | | 3 | SORT AGGREGATE | | 1 | 5 | | | | 4 | TABLE ACCESS FULL| T3 | 30000 | 146K| 74 (3)| 00:00:01 | ----------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 2 - filter(("T1"."N1">10000 AND "T1"."N3">))
As you can see, the cost of the query is the cost of the t1 tablescan plus the cost of running the t2 or t3 subquery once: 111 = 49 + 62, and 123 = 49 + 74.
(As a general guideline, recent versions of the optimizer tend to allow for subqueries by including “cost of subquery” * “number of times the optimizer thinks subquery will operate” – in this case the optimizer knows that the subquery will run exactly once).
But what happens when we test the query that applies both subqueries to the tablescan ?
----------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | ----------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | | | 50 (100)| | | 1 | SORT AGGREGATE | | 1 | 15 | | | |* 2 | TABLE ACCESS FULL | T1 | 975 | 14625 | 50 (4)| 00:00:01 | | 3 | SORT AGGREGATE | | 1 | 5 | | | | 4 | TABLE ACCESS FULL| T2 | 25000 | 122K| 62 (4)| 00:00:01 | | 5 | SORT AGGREGATE | | 1 | 5 | | | | 6 | TABLE ACCESS FULL| T3 | 30000 | 146K| 74 (3)| 00:00:01 | ----------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 2 - filter(("T1"."N1">10000 AND ("T1"."N2"> OR "T1"."N3">))) ----------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | ----------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | | | 50 (100)| | | 1 | SORT AGGREGATE | | 1 | 15 | | | |* 2 | TABLE ACCESS FULL | T1 | 975 | 14625 | 50 (4)| 00:00:01 | | 3 | SORT AGGREGATE | | 1 | 5 | | | | 4 | TABLE ACCESS FULL| T3 | 30000 | 146K| 74 (3)| 00:00:01 | | 5 | SORT AGGREGATE | | 1 | 5 | | | | 6 | TABLE ACCESS FULL| T2 | 25000 | 122K| 62 (4)| 00:00:01 | ----------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 2 - filter(("T1"."N1">10000 AND ("T1"."N3"> OR "T1"."N2">)))
The cost of the query in both cases is just the cost of the tablescan of t1 – the subqueries are, apparently, free. You can check from the predicate section, by the way, that the subqueries are applied in the order they appear in original statement, and confirm it by enablng extended SQL trace with wait stats, flushing the buffer cache, and then checking for the “db file scatterred read” waits for each tablescan.
Does anything change if the subqueries are not pushed ?
----------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | ----------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | | | 111 (100)| | | 1 | SORT AGGREGATE | | 1 | 15 | | | |* 2 | FILTER | | | | | | |* 3 | TABLE ACCESS FULL | T1 | 10001 | 146K| 50 (4)| 00:00:01 | | 4 | SORT AGGREGATE | | 1 | 5 | | | | 5 | TABLE ACCESS FULL| T2 | 25000 | 122K| 62 (4)| 00:00:01 | | 6 | SORT AGGREGATE | | 1 | 5 | | | | 7 | TABLE ACCESS FULL| T3 | 30000 | 146K| 74 (3)| 00:00:01 | ----------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 2 - filter(("T1"."N2"> OR "T1"."N3">)) 3 - filter("T1"."N1">10000) ----------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | ----------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | | | 124 (100)| | | 1 | SORT AGGREGATE | | 1 | 15 | | | |* 2 | FILTER | | | | | | |* 3 | TABLE ACCESS FULL | T1 | 10001 | 146K| 50 (4)| 00:00:01 | | 4 | SORT AGGREGATE | | 1 | 5 | | | | 5 | TABLE ACCESS FULL| T3 | 30000 | 146K| 74 (3)| 00:00:01 | | 6 | SORT AGGREGATE | | 1 | 5 | | | | 7 | TABLE ACCESS FULL| T2 | 25000 | 122K| 62 (4)| 00:00:01 | ----------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 2 - filter(("T1"."N3"> OR "T1"."N2">)) 3 - filter("T1"."N1">10000)
The two plans have different costs – and the cost is the cost of the tablescan of t1 plus the cost of just the first subquery in the filter predicate list.
The non-pushed subqueries show up another anomaly: you’ll notice that the t1 tablescan predicts a cardinality of 10,001 rows, but the FILTER operation doesn’t have an associated cardinality so we can’t see how many rows the optimizer thinks will survive the subqueries. So let’s run a query that allows us to see the surviving row estimate:
select max(n1) from ( select /*+ no_eliminate_oby */ t1.n1 from t1 where t1.n1 > 10000 and ( t1.n3 > (select /*+ no_push_subq */ avg(t3.n3) from t3) or t1.n2 > (select /*+ no_push_subq */ avg(t2.n2) from t2) ) order by n1 ) ; ------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | ------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | | | 126 (100)| | | 1 | SORT AGGREGATE | | 1 | 13 | | | | 2 | VIEW | | 10001 | 126K| 126 (5)| 00:00:01 | | 3 | SORT ORDER BY | | 10001 | 146K| 126 (5)| 00:00:01 | |* 4 | FILTER | | | | | | |* 5 | TABLE ACCESS FULL | T1 | 10001 | 146K| 50 (4)| 00:00:01 | | 6 | SORT AGGREGATE | | 1 | 5 | | | | 7 | TABLE ACCESS FULL| T3 | 30000 | 146K| 74 (3)| 00:00:01 | | 8 | SORT AGGREGATE | | 1 | 5 | | | | 9 | TABLE ACCESS FULL| T2 | 25000 | 122K| 62 (4)| 00:00:01 | ------------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 4 - filter(("T1"."N3"> OR "T1"."N2">)) 5 - filter("T1"."N1">10000)
As you can see, the SORT ORDER BY operation thinks it’s going to handle 10,001 rows – it looks as if the optimizer arithmetic hasn’t applied the usual subquery guess of 5% for the two subqueries. (When the subqueries were automatically pushed we saw a cardinality of 975 – which is 5% for subquery t2 plus (due to OR) 5% for subquery t3 minus 5% of 5% (= 0.25% giving 25 rows) for the overlap – which is the standard OR arithmetic)
tl;dr
Although the optimizer code has been enhanced in many places for dealing with subquery estimates there are still some odd errors and inconsistencies that you need to be aware of. The examples I’ve shown may not be particularly significant in terms of what they do but the pattern is one that you may recognise in more complex queries.
Update Aug 2020
I’ve just been prompted to review this note and re-run the tests on 19.3.0.0: there’s no change in the costs or cardinalities. The bug number mentioned in comment #1 below doesn’t appear in a search of MOS.
Thank you for such a wonderful post explaining the costing anomaly when using OR predicate in subqueries.
A Bug (BUG 22488215 – OPTIMIZER WRONG COST CALCULATION WHEN USING ‘OR’ PREDICATE) has been raised for this issue which is still under development team review.
Comment by ananthsriram — January 11, 2016 @ 1:02 pm GMT Jan 11,2016 |