Showing posts with label subquery factoring. Show all posts
Showing posts with label subquery factoring. Show all posts

Sunday, June 10, 2012

Report Generators And Query Transformations

Usually the Cost-Based Optimizer arrives at a reasonable execution plan if it gets the estimates regarding cardinality and data scattering / clustering right (if you want to learn more about that why not watch my Webinar available at "AllThingsOracle.com"?).

Here is an example I've recently come across where this wasn't case - the optimizer obviously preferred plans with a significantly higher cost.

The setup to reproduce the issue is simple:

create table t1 as select rownum as id , mod(rownum, 100) + 1 as attr1 , 'DESC' || to_char(mod(rownum, 100) + 1, 'TM') as attr2 , mod(rownum, 10) + 1 as attr3 , rpad('x', 100) as filler from dual connect by level <= 1000000 ; exec dbms_stats.gather_table_stats(null, 't1') create table t2 as select rownum as id , mod(rownum, 2) + 1 as attr1 , 'DESC' || to_char(mod(rownum, 2) + 1, 'TM') as attr2 , mod(rownum, 10) + 1 as attr3 , rpad('x', 100) as filler from dual connect by level <= 10000 ; exec dbms_stats.gather_table_stats(null, 't2', no_invalidate => false)

So we have two tables, and the smaller one of the tables basically was used as a "security" information to restrict the data from the larger table according to some user entitlement (apologies for the generic column and table names that don't make this more obvious).

The query that I'm looking at is the following:

with a as ( select * from t1 where attr3 in (select distinct attr3 from t2 where attr1 = 1) ), b as ( select attr1 as "attr1" , min(attr2) as "min_attr2" from a group by attr1 ) select "attr1" as "memberuniquename" , min("min_attr2") as "member" from b where 1 = 1 group by "attr1" ;

At first sight you might ask yourself, why should anyone write a query like that, in particular the duplicated aggregation step? The answer is of course, a human being very likely wouldn't write such a query, but this is what you get from some report generators based on generic meta data.

In principle the query is looking for a simple list of values from a table (in this case dimension tables that are browsed interactively), but restricted according to the "profile" of a particular user, here represented by the unique list of values of T2.ATTR3 restricted on T2.ATTR1 = 1.

There are a couple of design issues with the reproduced approach, but the query doesn't look too complex after all.

One specific issue of the query is that the MIN() aggregate allows Oracle a certain kind of flexibility when to apply the aggregate function: It could be applied on duplicated data and still generate the correct answer, something that is not necessarily possible with other aggregate functions like COUNT() for example.

One potential threat therefore comes from the fact that a query transformation of the IN clause to a kind of join can produce a huge intermediate result set since ATTR3 in both tables only has ten distinct values. Usually this is not a problem since the optimizer, provided it gets the estimates right, should recognize this pattern and calculate a corresponding increased cost for such plans, and therefore prefer other execution plans that avoid this in some way, for example by using a DISTINCT / GROUP BY operation early before a join, or simply using a semi join operation.

In fact, the query could be simplified to the following:

select attr1 as "memberuniquename" , min(attr2) as "member" from t1 where t1.attr3 in (select t2.attr3 from t2 where attr1 = 1) group by attr1 ;


The database version in question was 11.2.0.1, and if you run the simplified query, then you'll get the following execution plan:

Plan hash value: 1591942764 ---------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | ---------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 100 | 4500 | 4858 (3)| 00:00:59 | | 1 | HASH GROUP BY | | 100 | 4500 | 4858 (3)| 00:00:59 | |* 2 | HASH JOIN | | 708 | 31860 | 4857 (3)| 00:00:59 | | 3 | VIEW | VW_GBF_6 | 10 | 30 | 50 (4)| 00:00:01 | | 4 | HASH GROUP BY | | 10 | 60 | 50 (4)| 00:00:01 | |* 5 | TABLE ACCESS FULL| T2 | 5000 | 30000 | 48 (0)| 00:00:01 | | 6 | VIEW | VW_GBC_5 | 708 | 29736 | 4807 (3)| 00:00:58 | | 7 | HASH GROUP BY | | 708 | 9204 | 4807 (3)| 00:00:58 | | 8 | TABLE ACCESS FULL| T1 | 1000K| 12M| 4707 (1)| 00:00:57 | ---------------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 2 - access("ITEM_1"="ITEM_1") 5 - filter("ATTR1"=1)


The execution plan is reasonable and performs reasonable - the optimizer used the so called "Group By Placement" transformation which took one of the predicted approaches to apply the DISTINCT (transformed to a GROUP BY) as early as possible and arrived at the following query after transformation and costing, taken from the optimizer trace file and slightly re-formatted for better readability:

SELECT "VW_GBC_5"."ITEM_3" "ATTR1" , MIN("VW_GBC_5"."ITEM_2") "MIN(ATTR2)" FROM ( SELECT "T2"."ATTR3" "ITEM_1" FROM "T2" "T2" WHERE "T2"."ATTR1"=1 GROUP BY "T2"."ATTR3" ) "VW_GBF_6" , ( SELECT "T1"."ATTR3" "ITEM_1" , MIN("T1"."ATTR2") "ITEM_2" , "T1"."ATTR1" "ITEM_3" FROM "T1" "T1" GROUP BY "T1"."ATTR3" , "T1"."ATTR1" ) "VW_GBC_5" WHERE "VW_GBC_5"."ITEM_1"="VW_GBF_6"."ITEM_1" GROUP BY "VW_GBC_5"."ITEM_3" ;


which is funnily a bit similar to the original query initially mentioned.

If you however check the execution plan of the original query, you'll get the following:

Plan hash value: 3221309153 ------------------------------------------------------------------------------ | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | ------------------------------------------------------------------------------ | 0 | SELECT STATEMENT | | 100 | 3900 | 81386 (95)| 00:16:17 | | 1 | HASH GROUP BY | | 100 | 3900 | 81386 (95)| 00:16:17 | | 2 | VIEW | | 100 | 3900 | 81386 (95)| 00:16:17 | | 3 | HASH GROUP BY | | 100 | 1900 | 81386 (95)| 00:16:17 | |* 4 | HASH JOIN | | 500M| 9059M| 10222 (54)| 00:02:03 | |* 5 | TABLE ACCESS FULL| T2 | 5000 | 30000 | 48 (0)| 00:00:01 | | 6 | TABLE ACCESS FULL| T1 | 1000K| 12M| 4707 (1)| 00:00:57 | ------------------------------------------------------------------------------ Predicate Information (identified by operation id): --------------------------------------------------- 4 - access("ATTR3"="ATTR3") 5 - filter("ATTR1"=1)


This execution plan looks quite different, and it shows exactly the problem I've described - the optimizer has the option to postpone the aggregation after the join due to the specifics of the query: A MIN() aggregate can be applied late, but the problem can be seen from the cardinality estimate of the join - a whopping 500 million rows, that are crunched afterwards into 100 rows. This doesn't look like an efficient approach, and indeed at actual execution time a lot of CPU time (several minutes, depending on the CPU speed) gets used for handling that huge amount of intermediate data. It is probably a bug that it doesn't use at least a simple semi join to avoid the duplicates.

When I first saw this problem I was pretty sure that this is a typical problem of the impact of the WITH clause on query transformations in versions prior to 11.2.0.3 where that problem finally was addressed (to a very large extend, although not completely eliminated).

And indeed, when rewriting the original query to the corresponding one using inline views instead of Subquery Factoring / Common Table Expression:

select "attr1" as "memberuniquename" , min("min_attr2") as "member" from ( select attr1 as "attr1" , min(attr2) as "min_attr2" from ( select * from t1 where attr3 in ( select distinct attr3 from t2 where attr1 = 1 ) ) a group by attr1 ) b where 1 = 1 group by "attr1" ;


the following execution plan will be generated:

Plan hash value: 1699732834 ------------------------------------------------------------------------------------ | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | ------------------------------------------------------------------------------------ | 0 | SELECT STATEMENT | | 100 | 3900 | 4858 (3)| 00:00:59 | | 1 | HASH GROUP BY | | 100 | 3900 | 4858 (3)| 00:00:59 | | 2 | VIEW | | 100 | 3900 | 4858 (3)| 00:00:59 | | 3 | HASH GROUP BY | | 100 | 4500 | 4858 (3)| 00:00:59 | |* 4 | HASH JOIN | | 708 | 31860 | 4857 (3)| 00:00:59 | | 5 | VIEW | VW_GBF_8 | 10 | 30 | 50 (4)| 00:00:01 | | 6 | HASH GROUP BY | | 10 | 60 | 50 (4)| 00:00:01 | |* 7 | TABLE ACCESS FULL| T2 | 5000 | 30000 | 48 (0)| 00:00:01 | | 8 | VIEW | VW_GBC_7 | 708 | 29736 | 4807 (3)| 00:00:58 | | 9 | HASH GROUP BY | | 708 | 9204 | 4807 (3)| 00:00:58 | | 10 | TABLE ACCESS FULL| T1 | 1000K| 12M| 4707 (1)| 00:00:57 | ------------------------------------------------------------------------------------ Predicate Information (identified by operation id): --------------------------------------------------- 4 - access("ITEM_1"="ITEM_1") 7 - filter("ATTR1"=1)


which is almost identical to the execution plan of the simplified query. Notice also the huge difference in cost compared to the execution plan for the original query. A test on 11.2.0.3 showed the same execution plan without the re-write to inline views, so the changes introduced in 11.2.0.3 address this issue.

I've advised therefore to disable the usage of the WITH clause in the report generator, which was a matter of a simple click in the tool.

More Query Variations

To that point I thought this could be filed as just another example of the already known Subquery Factoring / Query Transformations issues, but it's not over yet - the report generator managed to produce more variations of the query. I liked in particular this one, which used the new setting disabling the WITH clause:

select "attr1" as "memberuniquename" , min("min_attr2") as "member" from ( select attr1 as "attr1" , min(attr2) as "min_attr2" from ( select * from t1 where attr3 in ( select distinct attr3 from t2 where attr1 = 1 ) ) a group by attr1 having 1 = 1 ) b group by "attr1" ;


Whereas in the previous example an innocent "WHERE 1 = 1" predicate was added and simply ignored by the optimizer, this time a seemingly innocent "HAVING 1 = 1" was added, and this identified another "weak spot" in the optimizer code, because now the execution plan generated was again the HASH JOIN with the huge intermediate result.

In fact, when changing the simplified query by adding the redundant HAVING clause like this:

select attr1 as "memberuniquename" , min(attr2) as "member" from t1 where t1.attr3 in (select t2.attr3 from t2 where attr1 = 1) group by attr1 having 1 = 1 ;


The bad execution plan can be reproduced, too. Even 11.2.0.3 doesn't fix this, so in this case one possible tactical workaround was to use the NO_UNNEST hint for the subquery because we knew the data and the number of distinct keys used as input/output to the subquery were always only a few, so we could benefit from filter subquery caching a lot. Notice that changing in the above query the MIN() aggregate to a COUNT() for example will change the join to a semi join, avoiding the problem of the huge set of intermediate duplicated data.

The strategic solution in this case of course was to use a correct data model that prevented the duplicates in the "security" table in first place. This prevented the huge intermediate result set even with when the "good" query transformations were not applied.

Footnote

Watch out if you upgrade to 11.2.0.3. Potentially a lot of execution plans using the WITH clause might change, and as you know, not always for the better.

Monday, January 14, 2008

Subquery factoring and cost based query transformation (CBQT)

Oracle 9i introduced the "subquery factoring" feature (the WITH clause), and also offers a optimizer feature called "complex view merging". Subquery factoring allows the database to optionally "materialize" the result of a WITH clause as global temporary table (GTT), which can be beneficial if the WITH clause is referenced more than once in the main statement.
Complex views are view definitions or inline views that contain e.g. distinct or group by clauses.

Oracle 10g in addition introduced the "Cost based query transformation" (CBQT) feature that allows Oracle to work out if e.g. applying complex view merging is beneficial by comparing
the cost of the transformed with the cost of the original query, whereas 9i - if possible - always applies the complex view merging even if the transformed query is not superior to the original one in terms of costs.

In order to take advantage of the potential benefit of "subquery factoring" one might come to the conclusion that it's a good idea to use the subquery factoring as often as possible, since in theory it should just offer advantages. In case the instantiation of the query result is not used the plan should be not worse than the one generated when using traditional views/inline views.

Unfortunately test cases show that the usage of subquery factoring actually disables certain options otherwise available to the optimizer, in particular the "Cost based query transformation" optimizer feature seems to be skipped if the subquery factoring is used.

This is a pitty as it means that you have to be careful with the usage of subquery factoring, because rather than potentially improving the performance it might make things actually worse under certain circumstances.

This still holds true even for the current Oracle release 11g, it looks like things haven't changed there.

The following test case ran on Oracle 11.1.0.6.0 Windows 32-Bit (default database installation) shows the different execution plans generated by the optimizer when optimizing two similar statements using traditional inline views and subquery factoring.

Tests on 10.2.0.3 Windows 32-Bit (default database installation) have shown similar results.

The test case setup uses two generic tables that are populated using ALL_OBJECTS. The first table contains in a default 11g installation approximately 50.000 records, the second one 10 records.

create table complex_view_merging_test as
select
trunc(sqrt(rownum-1)) as skewed_data,
rownum-1 as id,
lpad(rownum-1,10) id_char,
rpad('x',50, 'x') as filler
from
all_objects;

create table complex_view_merging_test2 as
select * from complex_view_merging_test
where skewed_data between 0 and 2;

begin
dbms_stats.gather_table_stats(
ownname=>USER,
tabname=>'complex_view_merging_test',
method_opt=>'FOR ALL COLUMNS SIZE 1');

dbms_stats.gather_table_stats(
ownname=>USER,
tabname=>'complex_view_merging_test2',
method_opt=>'FOR ALL COLUMNS SIZE 1');

end;
/

The tables are analyzed using simple options of DBMS_STATS, even omitting histograms in order to keep things as simple as possible, although using histograms won't change the outcome in this case.

This is the query using traditional inline views:

select * from (
select skewed_data, id, count(*) as cnt
from complex_view_merging_test
group by skewed_data, id
) a, (
select * from complex_view_merging_test2
) b
where a.id = b.id;

Since the second table just contains a fraction of the first query, it is rather obvious that it is probably better to first join the two tables in order to eliminate most of the rows and afterwards apply the group by to the greatly reduced result set. This can be achieved by the "Complex view merging" feature and the cost of the merged statement should reflect this as it should be less than the cost of the unmerged statement that first applies the group by to the larger table and afterwards performs the join, since the group by in this case does not reduce the result very much.

As a side note it's worth to mention that when using the parallel query feature things might be quite different in case you have a HASH distribution used by the "Hash Join" operation and the join key criteria is very skewed. Since this results in a skewed distribution of the data to the parallel query slaves which means that most of the work will be done by only a few of the parallel slaves rendering the parallel operation rather inefficient in this particular case it could be more advantageous to potentially reduce the skewness first by applying the grouping before joining the resulting set.

The plan generated looks like this:

Planִhashִvalue:ִ3999694253

--------------------------------------------------------------------------------------------------
|ִIdִִ|ִOperationִִִִִִִִִִִ|ִNameִִִִִִִִִִִִִִִִִִִִִִִ|ִRowsִִ|ִBytesִ|ִCostִ(%CPU)|ִTimeִִִִִ|
--------------------------------------------------------------------------------------------------
|ִִִ0ִ|ִSELECTִSTATEMENTִִִִ|ִִִִִִִִִִִִִִִִִִִִִִִִִִִִ|ִִִִִ9ִ|ִִִ684ִ|ִִִ159ִִִ(2)|ִ00:00:02ִ|
|ִִִ1ִ|ִִHASHִGROUPִBYִִִִִִ|ִִִִִִִִִִִִִִִִִִִִִִִִִִִִ|ִִִִִ9ִ|ִִִ684ִ|ִִִ159ִִִ(2)|ִ00:00:02ִ|
|*ִִ2ִ|ִִִHASHִJOINִִִִִִִִִ|ִִִִִִִִִִִִִִִִִִִִִִִִִִִִ|ִִִִִ9ִ|ִִִ684ִ|ִִִ158ִִִ(1)|ִ00:00:02ִ|
|ִִִ3ִ|ִִִִTABLEִACCESSִFULL|ִCOMPLEX_VIEW_MERGING_TEST2ִ|ִִִִִ9ִ|ִִִ603ִ|ִִִִִ2ִִִ(0)|ִ00:00:01ִ|
|ִִִ4ִ|ִִִִTABLEִACCESSִFULL|ִCOMPLEX_VIEW_MERGING_TESTִִ|ִ53755ִ|ִִִ472K|ִִִ156ִִִ(1)|ִ00:00:02ִ|
--------------------------------------------------------------------------------------------------

PredicateִInformationִ(identifiedִbyִoperationִid):
---------------------------------------------------
ִ
ִִִ2ִ-ִaccess("ID"="COMPLEX_VIEW_MERGING_TEST2"."ID")

It looks like the optimizer actually has come to the conclusion that merging the view, that means performing the join first and group afterwards, is the most efficient way.

This is the same statement, but using subquery factoring:

with a as (
select skewed_data, id, count(*) as cnt
from complex_view_merging_test
group by skewed_data, id
), b as (
select * from complex_view_merging_test2
)
select * from a, b
where a.id = b.id;

The generated execution plan looks like this:

Planִhashִvalue:ִ1393518913

-----------------------------------------------------------------------------------------------------------
|ִIdִִ|ִOperationִִִִִִִִִִִִ|ִNameִִִִִִִִִִִִִִִִִִִִִִִ|ִRowsִִ|ִBytesִ|TempSpc|ִCostִ(%CPU)|ִTimeִִִִִ|
-----------------------------------------------------------------------------------------------------------
|ִִִ0ִ|ִSELECTִSTATEMENTִִִִִ|ִִִִִִִִִִִִִִִִִִִִִִִִִִִִ|ִִִִִ9ִ|ִִִ954ִ|ִִִִִִִ|ִִִ367ִִִ(2)|ִ00:00:05ִ|
|*ִִ1ִ|ִִHASHִJOINִִִִִִִִִִִ|ִִִִִִִִִִִִִִִִִִִִִִִִִִִִ|ִִִִִ9ִ|ִִִ954ִ|ִִִִִִִ|ִִִ367ִִִ(2)|ִ00:00:05ִ|
|ִִִ2ִ|ִִִTABLEִACCESSִFULLִִ|ִCOMPLEX_VIEW_MERGING_TEST2ִ|ִִִִִ9ִ|ִִִ603ִ|ִִִִִִִ|ִִִִִ2ִִִ(0)|ִ00:00:01ִ|
|ִִִ3ִ|ִִִVIEWִִִִִִִִִִִִִִִ|ִִִִִִִִִִִִִִִִִִִִִִִִִִִִ|ִ53755ִ|ִִ2047K|ִִִִִִִ|ִִִ364ִִִ(1)|ִ00:00:05ִ|
|ִִִ4ִ|ִִִִHASHִGROUPִBYִִִִִ|ִִִִִִִִִִִִִִִִִִִִִִִִִִִִ|ִ53755ִ|ִִִ472K|ִִ2120K|ִִִ364ִִִ(1)|ִ00:00:05ִ|
|ִִִ5ִ|ִִִִִTABLEִACCESSִFULL|ִCOMPLEX_VIEW_MERGING_TESTִִ|ִ53755ִ|ִִִ472K|ִִִִִִִ|ִִִ156ִִִ(1)|ִ00:00:02ִ|
-----------------------------------------------------------------------------------------------------------

PredicateִInformationִ(identifiedִbyִoperationִid):
---------------------------------------------------
ִ
ִִִ1ִ-ִaccess("A"."ID"="COMPLEX_VIEW_MERGING_TEST2"."ID")

The generated plan is more expensive than the one generated using the statement without subquery factoring. In order to get the same plan as before, the "merge" hint can be used to force the view merge transformation to take place:

with a as (
select /*+ merge */ skewed_data, id, count(*) as cnt
from complex_view_merging_test
group by skewed_data, id
), b as (
select * from complex_view_merging_test2
)
select * from a, b
where a.id = b.id;

This results in the cheaper plan seen before, but the optimizer does not come up with this plan on its own. Examining the 10053 optimizer trace of the subquery factoring statement, the following output can be found multiple times:

Considering Query Transformations on query block (#0)
**************************
Query transformations (QT)
**************************
CBQT: copy not possible on query block (#0) because linked to with clause
CBQT bypassed for query block (#0): Cannot copy query block.
CBQT: Validity checks failed for 6phrc6zz5g12p.

Whereas looking at the trace of the first statement, the following can be found:

Considering Query Transformations on query block (#0)
**************************
Query transformations (QT)
**************************
CBQT: Validity checks passed for 24kj5u4mxr19r.
.
.
.
CVM: CBQT Marking query block as valid for CVM.
CVM: Merging complex view (#2) into (#1).
.
.
.

So this seems to confirm the assumption that the usage of the subquery factoring effectively disables some other features of the optimizer, in this particular case the complex view merging of the cost based query transformation.

As of now you probably have to manually test which of the two approaches might offer the best performance when considering the subquery factoring feature. It is advisable to use the subquery factoring option not too blindly, especially if the query contains views to merge.