general46
general46

Reputation: 820

Are functions considered in Execution Plan?

How will the execution plan be generated when the query has PL/SQL functions(user-defined functions) in SELECT or WHERE clauses? Does it calculate the cost for those functions also and show it in the execution plan or the functions are just ignored?

Thanks in advance for your help.

Upvotes: 4

Views: 1223

Answers (1)

Jon Heller
Jon Heller

Reputation: 36902

User generated functions directly contribute little or no cost when they are used in either the SELECT or the WHERE clause. If we want the optimizer to make decisions based on the cost of functions, we must manually set a cost with the ASSOCIATE STATISTICS command.

Sample Schema

For this example, create the following medium sized table, and two simple functions - one that is obviously fast, and one that is obviously slow.

create table test1 as
select mod(level, 10) a, mod(level, 10) b
from dual
connect by level <= 100000;

begin
    dbms_stats.gather_table_stats(user, 'test1');
end;
/

create or replace function fast_function(p_number number) return number is
begin
    return p_number;
end;
/

create or replace function slow_function(p_number number) return number is
    v_count number;
begin
    select count(*)
    into v_count
    from all_tables;
    return v_count;
end;
/

Functions in SELECT clause - no cost

Calling the function in the SELECT clause does not change the cost at all. The below three queries SELECT a literal, the fast function, and the slow function:

explain plan for select a from test1;
select * from table(dbms_xplan.display);

explain plan for select fast_function(a) from test1;
select * from table(dbms_xplan.display);

explain plan for select slow_function(a) from test1;
select * from table(dbms_xplan.display);

But all queries generate the same execution plan:

---------------------------------------------------------------------------
| Id  | Operation         | Name  | Rows  | Bytes | Cost (%CPU)| Time     |
---------------------------------------------------------------------------
|   0 | SELECT STATEMENT  |       |   100K|   292K|    47   (3)| 00:00:01 |
|   1 |  TABLE ACCESS FULL| TEST1 |   100K|   292K|    47   (3)| 00:00:01 |
---------------------------------------------------------------------------

Functions in WHERE clause - little cost

When calling the functions in the WHERE clause instead of a literal, the cost slightly increases from 48 to 70. But there is no cost difference between the fast function and the slow function.

explain plan for select * from test1 where a = b;
select * from table(dbms_xplan.display);

---------------------------------------------------------------------------
| Id  | Operation         | Name  | Rows  | Bytes | Cost (%CPU)| Time     |
---------------------------------------------------------------------------
|   0 | SELECT STATEMENT  |       | 10000 | 60000 |    48   (5)| 00:00:01 |
|*  1 |  TABLE ACCESS FULL| TEST1 | 10000 | 60000 |    48   (5)| 00:00:01 |
---------------------------------------------------------------------------

explain plan for select * from test1 where fast_function(a) = b;
select * from table(dbms_xplan.display);

---------------------------------------------------------------------------
| Id  | Operation         | Name  | Rows  | Bytes | Cost (%CPU)| Time     |
---------------------------------------------------------------------------
|   0 | SELECT STATEMENT  |       | 10000 | 60000 |    70  (35)| 00:00:01 |
|*  1 |  TABLE ACCESS FULL| TEST1 | 10000 | 60000 |    70  (35)| 00:00:01 |
---------------------------------------------------------------------------

explain plan for select * from test1 where slow_function(a) = b;
select * from table(dbms_xplan.display);

---------------------------------------------------------------------------
| Id  | Operation         | Name  | Rows  | Bytes | Cost (%CPU)| Time     |
---------------------------------------------------------------------------
|   0 | SELECT STATEMENT  |       | 10000 | 60000 |    70  (35)| 00:00:01 |
|*  1 |  TABLE ACCESS FULL| TEST1 | 10000 | 60000 |    70  (35)| 00:00:01 |
---------------------------------------------------------------------------

ASSOCIATE STATISTICS

We can set the cpu_cost, io_cost, and network_cost for each call to the function. There's probably a way to find those specific costs using tracing, but the cost is an internal magic number that's hard to understand, and the optimizer generally only needs numbers within an order of magnitude to make good decisions. I found the total cost of the query inside the slow function, 1000, and divided it equally into the cpu_cost and io_cost like this:

associate statistics with functions slow_function default cost(500,500,0);

Now the total cost for the plan increases dramatically from 70 to 100,000,000:

explain plan for select * from test1 where b = slow_function(b);
select * from table(dbms_xplan.display);

---------------------------------------------------------------------------
| Id  | Operation         | Name  | Rows  | Bytes | Cost (%CPU)| Time     |
---------------------------------------------------------------------------
|   0 | SELECT STATEMENT  |       | 10000 | 60000 |   100M  (1)| 01:05:07 |
|*  1 |  TABLE ACCESS FULL| TEST1 | 10000 | 60000 |   100M  (1)| 01:05:07 |
---------------------------------------------------------------------------

More importantly, Oracle can use this cost information to run the functions in the right order. In the below query, Oracle runs the fast function first, which costs almost nothing, and then runs the slow function on the remaining rows.

(It's a bit difficult to tell the order of function execution. The lower overall cost implies how the functions are run. And the order of the functions in the FILTER is another sign. In regular SQL, the two sides of an AND predicate could be run in any order. In an explain plan, the execution order seems to always be left-to-right.)

explain plan for select * from test1 where a = fast_function(a) and b = slow_function(b);
select * from table(dbms_xplan.display);

---------------------------------------------------------------------------
| Id  | Operation         | Name  | Rows  | Bytes | Cost (%CPU)| Time     |
---------------------------------------------------------------------------
|   0 | SELECT STATEMENT  |       |  1000 |  6000 |    10M  (1)| 00:06:31 |
|*  1 |  TABLE ACCESS FULL| TEST1 |  1000 |  6000 |    10M  (1)| 00:06:31 |
---------------------------------------------------------------------------
 
Predicate Information (identified by operation id):
---------------------------------------------------
 
   1 - filter("A"="FAST_FUNCTION"("A") AND "B"="SLOW_FUNCTION"("B"))

Selectivity

Despite the name "cost based optimizer", we should probably worry more about the cardinality than the cost. The number of rows returned by predicates drives most execution plan choices. Oracle makes a few default guesses about user defined functions. For example, in the below query, Oracle assumes that the function will only satisfy 1% of the rows - that's why the "Rows" in the execution plan says 1000 instead of 100000.

explain plan for select * from test1 where fast_function(a) = 1;
select * from table(dbms_xplan.display);

---------------------------------------------------------------------------
| Id  | Operation         | Name  | Rows  | Bytes | Cost (%CPU)| Time     |
---------------------------------------------------------------------------
|   0 | SELECT STATEMENT  |       |  1000 |  6000 |    70  (35)| 00:00:01 |
|*  1 |  TABLE ACCESS FULL| TEST1 |  1000 |  6000 |    70  (35)| 00:00:01 |
---------------------------------------------------------------------------

If we know that the function is much more selective, for example if we know that the function is more likely to only match 0.1% of all rows, we can also use ASSOCIATE STATISTICS to set the default selectivity. The below command sets the selectivity and then the number of rows drops from 1000 to 100.

associate statistics with functions fast_function default selectivity 0.1;
explain plan for select * from test1 where fast_function(a) = 1;
select * from table(dbms_xplan.display);

---------------------------------------------------------------------------
| Id  | Operation         | Name  | Rows  | Bytes | Cost (%CPU)| Time     |
---------------------------------------------------------------------------
|   0 | SELECT STATEMENT  |       |   100 |   600 |    70  (35)| 00:00:01 |
|*  1 |  TABLE ACCESS FULL| TEST1 |   100 |   600 |    70  (35)| 00:00:01 |
---------------------------------------------------------------------------

In our simple plans, the cardinality doesn't matter. But in realistic queries, horrible cardinality estimates cause a chain reaction of bad decisions that leads to slow queries. Helping the optimizer make good cardinality estimates is often the most important part of performance tuning.

Other kinds of functions and statistics

This already-long-answer still only scratches the surface of how functions can affect execution plans. Table functions, functions that return rows of data, are a whole other topic. And I'd bet there are dynamic reoptimization features in newer Oracle versions that will help improve the second or third execution, after the optimizer has learned from its mistakes.

I hope I didn't discourage you from using custom functions. The vast majority of the time, Oracle will make the right decisions without any effort. And when it doesn't, there are mechanisms to help correct those mistakes.

Upvotes: 4

Related Questions