142 lines
9.8 KiB
SQL
142 lines
9.8 KiB
SQL
--
|
|
-- generate SQL for the FRP spreadsheet from the most recent plan in the plan_table
|
|
--
|
|
-- usage is: @SHOWPLANFRPSPREADSHEETCODE11G
|
|
--
|
|
|
|
col actual_frp format 999.0
|
|
col plan_frp format 999.0
|
|
col select_id noprint
|
|
|
|
with
|
|
--
|
|
-- generates a FRP SPREADSHEET query using plan table information for an EXPLAINED query
|
|
--
|
|
--
|
|
-- THIS CODE IS DEPENDENT UPON WHAT IS IN THE PLAN TABLE
|
|
-- among other things this means that if oracle changes the contents of this table, this query may stop working
|
|
--
|
|
-- several possible flaws can prevent this code from generating an executable SQL SELECT
|
|
--
|
|
-- 1. bind variables: the corresponding select here must be modified to group on the bind column and select an round(avg(count(*))
|
|
-- 2. join predicates: if any are used along with constant tests in a plan step, they must be manaully edited out of the correspoding select here
|
|
-- 3. packaged functions: column=functioncall predicates can be dropped in which case filtered_cardinality will be affected, check it if you query has these
|
|
-- 4. outer join is not supported. These should be removed. If the case expression becomes empty use count(*).
|
|
-- 5. correlated subqueries are confusing. This is because the appear as queries with bind variables. Test like #1 but ingnore their results.
|
|
--
|
|
--
|
|
-- get raw_data from the plan_table for each table reference in the query plan
|
|
-- a table may be used more than once in which case there will be more than one row returned here
|
|
-- this is managed by using ID so that we know the plan step the table reference refers to
|
|
-- note that some plan steps may be index lookups so in this section we translate the index to its underlying table
|
|
--
|
|
table_list as (
|
|
select a.id,a.object_owner table_owner,a.object_name table_name,a.access_predicates,a.filter_predicates,a.object_alias,a.cardinality
|
|
from plan_table a
|
|
,dba_tables b
|
|
where b.owner = a.object_owner
|
|
and b.table_name = a.object_name
|
|
and a.plan_id = (select max(plan_id) from plan_table)
|
|
union all
|
|
select a.id,b.table_owner,b.table_name object_name,a.access_predicates,a.filter_predicates,a.object_alias,a.cardinality
|
|
from plan_table a
|
|
,dba_indexes b
|
|
where b.owner = a.object_owner
|
|
and b.index_name = a.object_name
|
|
and a.plan_id = (select max(plan_id) from plan_table)
|
|
)
|
|
--
|
|
-- given the raw data for tables, modify the predicates so that we only see predicates for constant tests, no join predicates
|
|
-- join predicates are not used in FRP analysis
|
|
-- this is a bit of a hack as I never took the COMPILER and PARSER classes in school, basically this means it is almost 100%right
|
|
-- what we call "close enough for jazz"
|
|
--
|
|
, modified_table_list as (
|
|
select id,table_owner,table_name,object_alias,cardinality
|
|
,case when
|
|
instr(replace(access_predicates,'"="'),'=') > 0 or
|
|
instr(replace(access_predicates,'">"'),'>') > 0 or
|
|
instr(replace(access_predicates,'"<"'),'<') > 0 or
|
|
instr(replace(access_predicates,'">="'),'>=') > 0 or
|
|
instr(replace(access_predicates,'"<="'),'<=') > 0 or
|
|
instr(replace(access_predicates,'"!="'),'!=') > 0 or
|
|
instr(replace(access_predicates,'"<>"'),'<>') > 0 or
|
|
instr(replace(access_predicates,'" LIKE "'),' LIKE ') > 0 or
|
|
instr(replace(access_predicates,'" BETWEEN "'),' BETWEEN ') > 0 or
|
|
instr(replace(access_predicates,'" IN ("'),' IN (') > 0 or
|
|
instr(replace(access_predicates,'" NOT LIKE "'),' NOT LIKE ') > 0 or
|
|
instr(replace(access_predicates,'" NOT BETWEEN "'),' NOT BETWEEN ') > 0 or
|
|
instr(replace(access_predicates,'" NOT IN ("'),' NOT IN (') > 0
|
|
then access_predicates
|
|
end access_predicates
|
|
,case when
|
|
instr(replace(filter_predicates,'"="'),'=') > 0 or
|
|
instr(replace(filter_predicates,'">"'),'>') > 0 or
|
|
instr(replace(filter_predicates,'"<"'),'<') > 0 or
|
|
instr(replace(filter_predicates,'">="'),'>=') > 0 or
|
|
instr(replace(filter_predicates,'"<="'),'<=') > 0 or
|
|
instr(replace(filter_predicates,'"!="'),'!=') > 0 or
|
|
instr(replace(filter_predicates,'"<>"'),'<>') > 0 or
|
|
instr(replace(filter_predicates,'" LIKE "'),' LIKE ') > 0 or
|
|
instr(replace(filter_predicates,'" BETWEEN "'),' BETWEEN ') > 0 or
|
|
instr(replace(filter_predicates,'" IN ("'),' IN (') > 0 or
|
|
instr(replace(filter_predicates,'" NOT LIKE "'),' NOT LIKE ') > 0 or
|
|
instr(replace(filter_predicates,'" NOT BETWEEN "'),' NOT BETWEEN ') > 0 or
|
|
instr(replace(filter_predicates,'" NOT IN ("'),' NOT IN (') > 0
|
|
then filter_predicates
|
|
end filter_predicates
|
|
from table_list
|
|
)
|
|
--
|
|
-- do the final massaging of the raw data
|
|
-- in particular, get the true alias for each table, get data from dba_tables, generate an actual predicate we can test with
|
|
--
|
|
, plan_info as
|
|
(
|
|
select
|
|
id
|
|
, table_owner
|
|
, table_name
|
|
, substr(object_alias,1,instr(object_alias,'@')-1) table_alias
|
|
, cardinality
|
|
, (select num_rows from dba_tables where dba_tables.owner = modified_table_list.table_owner and dba_tables.table_name = modified_table_list.table_name) num_rows
|
|
, case
|
|
when access_predicates is null and filter_predicates is null then null
|
|
when access_predicates is null and filter_predicates is not null then filter_predicates
|
|
when access_predicates is not null and filter_predicates is null then access_predicates
|
|
when access_predicates is not null and filter_predicates is not null and access_predicates != filter_predicates then access_predicates||' and '||filter_predicates
|
|
else access_predicates
|
|
end predicate
|
|
from modified_table_list
|
|
)
|
|
--
|
|
-- look for places where indexes are accessed followed by table acces by rowid
|
|
-- combine the two lines into one
|
|
--
|
|
, combined_plan_info as (
|
|
select plan_info.table_owner,plan_info.table_name,plan_info.table_alias,plan_info.cardinality,plan_info.num_rows
|
|
,min(plan_info.id) id
|
|
,listagg(plan_info.predicate,' and ') within group (order by id) predicate
|
|
from plan_info
|
|
group by plan_info.table_owner,plan_info.table_name,plan_info.table_alias,plan_info.cardinality,plan_info.num_rows
|
|
)
|
|
--
|
|
-- give us a SQL statement that for each table reference, both counts all rows and counts only rows that pass the filter predictes
|
|
-- then do the math needed to generate an FRP SPREADSHEET
|
|
-- this version (4) only scans each table once instead of twice like the old versions
|
|
--
|
|
select 1 select_id,'with' sqltext from dual union all
|
|
select 2 select_id,' frp_data as (' from dual union all
|
|
select 3 select_id,' select '''||lpad(id,5,' ')||''' id,'''||table_owner||''' table_owner,'''||table_name||''' table_name,'''||table_alias||''' table_alias,'||nvl(to_char(num_rows),'cast(null as number)')||' num_rows,count(*) rowcount,'||cardinality||' cardinality,'||decode(predicate,null,'cast(null as number)','count(case when '||predicate||' then 1 end)')||' filtered_cardinality from '||table_owner||'.'||table_name||' '||table_alias||' union all'
|
|
from combined_plan_info
|
|
union all
|
|
select 4 select_id,' select null,null,null,null,null,null,null,null from dual' from dual union all
|
|
select 5 select_id,' )' from dual union all
|
|
select 6 select_id,'select frp_data.*,round(frp_data.filtered_cardinality/case when frp_data.rowcount = 0 then cast(null as number) else frp_data.rowcount end*100,1) actual_frp,decode(frp_data.filtered_cardinality,null,cast(null as number),round(frp_data.cardinality/case when frp_data.num_rows = 0 then cast(null as number) else frp_data.num_rows end*100,1)) plan_frp' from dual union all
|
|
select 7 select_id,'from frp_data' from dual union all
|
|
select 8 select_id,'where id is not null' from dual union all
|
|
select 9 select_id,'order by frp_data.id' from dual union all
|
|
select 10 select_id,'/' from dual
|
|
order by 1
|
|
/
|