Let's create two tables for this demo:
SQL> create table t_parent 2 as 3 select level id 4 from dual 5 connect by level<=10; Table created. SQL> SQL> alter table t_parent 2 add constraint t_parent_pk primary key(id); Table altered. SQL> SQL> create table t_child 2 as 3 select level parent_id, dummy a, dummy b 4 from dual 5 connect by level<=10; Table created. SQL> SQL> alter table t_child 2 add constraint t_child_parent_fk foreign key(parent_id) references t_parent; Table altered. SQL> SQL> create index t_child_parent_fk_i on t_child(parent_id); Index created.The following query was run by the application (it's an oversimplified version of the actual query):
select id,
(select count(a) from t_child where parent_id = id) a_count,
(select count(b) from t_child where parent_id = id) b_count
from t_parent
where 1 = :need_to_run;
The variable NEED_TO_RUN is to define whether the actual query should be executed. The query was coming from an APEX application.When the APEX page loads, it sets that NEED_TO_RUN variable to 0 or NULL and fires the query - it is supposed not to return anything.
Only after the user clicks on a certain tab, it leads to setting that NEED_TO_RUN variable to 1 and the query is reexecuted - this time it might return some rows.
Let's see how it works:
SQL> var need_to_run number
SQL>
SQL> select /*+ gather_plan_statistics*/
2 id,
3 (select count(a) from t_child where parent_id = id) a_count,
4 (select count(b) from t_child where parent_id = id) b_count
5 from t_parent
6 where 1 = :need_to_run;
no rows selected
SQL>
SQL> select * from table(dbms_xplan.display_cursor( format=> 'allstats last'));
PLAN_TABLE_OUTPUT
-------------------------------------------------------------------------------------------------------------------
SQL_ID bk2mgzbdury0m, child number 0
-------------------------------------
select /*+ gather_plan_statistics*/ id, (select count(a)
from t_child where parent_id = id) a_count, (select count(b)
from t_child where parent_id = id) b_count from t_parent where 1 =
:need_to_run
Plan hash value: 2339616900
------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time |
------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 0 |00:00:00.01 |
| 1 | SORT AGGREGATE | | 0 | 1 | 0 |00:00:00.01 |
| 2 | TABLE ACCESS BY INDEX ROWID BATCHED| T_CHILD | 0 | 1 | 0 |00:00:00.01 |
|* 3 | INDEX RANGE SCAN | T_CHILD_PARENT_FK_I | 0 | 1 | 0 |00:00:00.01 |
| 4 | SORT AGGREGATE | | 0 | 1 | 0 |00:00:00.01 |
| 5 | TABLE ACCESS BY INDEX ROWID BATCHED| T_CHILD | 0 | 1 | 0 |00:00:00.01 |
|* 6 | INDEX RANGE SCAN | T_CHILD_PARENT_FK_I | 0 | 1 | 0 |00:00:00.01 |
|* 7 | FILTER | | 1 | | 0 |00:00:00.01 |
| 8 | INDEX FULL SCAN | T_PARENT_PK | 0 | 10 | 0 |00:00:00.01 |
------------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
3 - access("PARENT_ID"=:B1)
6 - access("PARENT_ID"=:B1)
7 - filter(1=:NEED_TO_RUN)
30 rows selected.
Oracle just evaluates the filter condition in line 7 and does not go any further.The query is in fact accesses T_CHILD table twice using the same access method, so that I suggested instead of querying it twice to query it just once.
The whole query was a little bit complex with several outer joins and a lateral view seemed to be a good fit.
The same query rewritten for using a lateral view is below:
SQL> select /*+ gather_plan_statistics*/p.id,
2 c.a_count,
3 c.b_count
4 from t_parent p,
5 lateral(
6 select count(a) a_count,
7 count(b) b_count
8 from t_child
9 where parent_id = p.id
10 ) c
11 where 1 = :need_to_run;
no rows selected
SQL>
SQL> select * from table(dbms_xplan.display_cursor( format=> 'allstats last'));
PLAN_TABLE_OUTPUT
---------------------------------------------------------------------------------------------------------------------------
SQL_ID g6pqun59pf178, child number 0
-------------------------------------
select /*+ gather_plan_statistics*/p.id, c.a_count,
c.b_count from t_parent p, lateral( select count(a)
a_count, count(b) b_count from t_child
where parent_id = p.id ) c where 1 = :need_to_run
Plan hash value: 1581593935
-------------------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
-------------------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 0 |00:00:00.01 | 5 |
| 1 | NESTED LOOPS | | 1 | 10 | 0 |00:00:00.01 | 5 |
| 2 | INDEX FULL SCAN | T_PARENT_PK | 1 | 10 | 10 |00:00:00.01 | 1 |
| 3 | VIEW | VW_LAT_A18161FF | 10 | 1 | 0 |00:00:00.01 | 4 |
|* 4 | FILTER | | 10 | | 0 |00:00:00.01 | 4 |
| 5 | SORT AGGREGATE | | 10 | 1 | 10 |00:00:00.01 | 4 |
| 6 | TABLE ACCESS BY INDEX ROWID BATCHED| T_CHILD | 10 | 1 | 10 |00:00:00.01 | 4 |
|* 7 | INDEX RANGE SCAN | T_CHILD_PARENT_FK_I | 10 | 1 | 10 |00:00:00.01 | 3 |
-------------------------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
4 - filter(1=:NEED_TO_RUN)
7 - access("PARENT_ID"="P"."ID")
28 rows selected.
There are a few remarkable things happened here:- The filter is pushed down to the lateral view
- Both the T_PARENT_TABLE and T_CHILD table are accessed first, then the filter is evaluated
- Even when we have that filter in line 4, why bother to run any rowsources beneath it?
The query with a lateral view was transformed into this:
Final query after transformations:******* UNPARSED QUERY IS *******
SELECT "P"."ID" "ID","VW_LAT_A18161FF"."A_COUNT_0" "A_COUNT","VW_LAT_A18161FF"."B_COUNT_1" "B_COUNT"
FROM "TC"."T_PARENT" "P",
LATERAL( (
SELECT COUNT("T_CHILD"."A") "A_COUNT_0",COUNT("T_CHILD"."B") "B_COUNT_1"
FROM "TC"."T_CHILD" "T_CHILD"
WHERE "T_CHILD"."PARENT_ID"="P"."ID"
HAVING 1=:B1)) "VW_LAT_A18161FF"
Hence, that filter 1 = :NEED_TO_RUN has been pushed down to that lateral view and became the HAVING 1=:B1 condition.The solution is quite simple. The query has been rewritten like this:
select /*+ gather_plan_statistics*/p.id,
c.a_count,
c.b_count
from t_parent p,
lateral(
select count(a) a_count,
count(b) b_count
from t_child
where parent_id = p.id
)(+) c
where 1 = :need_to_run;
It has the following execution plan in which the filter predicate is evaluated as early as possible:----------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time |
----------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 0 |00:00:00.01 |
|* 1 | FILTER | | 1 | | 0 |00:00:00.01 |
| 2 | MERGE JOIN OUTER | | 0 | 10 | 0 |00:00:00.01 |
| 3 | INDEX FULL SCAN | T_PARENT_PK | 0 | 10 | 0 |00:00:00.01 |
| 4 | BUFFER SORT | | 0 | 1 | 0 |00:00:00.01 |
| 5 | VIEW | VW_LAT_A18161FF | 0 | 1 | 0 |00:00:00.01 |
| 6 | SORT AGGREGATE | | 0 | 1 | 0 |00:00:00.01 |
| 7 | TABLE ACCESS BY INDEX ROWID BATCHED| T_CHILD | 0 | 1 | 0 |00:00:00.01 |
|* 8 | INDEX RANGE SCAN | T_CHILD_PARENT_FK_I | 0 | 1 | 0 |00:00:00.01 |
----------------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
1 - filter(1=:NEED_TO_RUN)
8 - access("PARENT_ID"="P"."ID")
There is also an option to set _optimizer_filter_pushdown to false, however, we went for the first solution and replaced LATERAL(query) with LATERAL(query)(+).That's another example when the optimizer gets overly aggressive in rewritting a query, so that in this particular case it outsmarted itself.
Комментариев нет:
Отправить комментарий
Примечание. Отправлять комментарии могут только участники этого блога.