Assuming everything else has been tuned to perfection, what’s the best you can do while calling PL/SQL functions from SQL ? Here’s a little code to create a table with some data, and a function that we can use to start an investigation:
rem rem Script: 12c_function_options.sql rem Author: Jonathan Lewis rem Dated: Sep 2015 rem create table t1 nologging pctfree 0 as with generator as ( select --+ materialize rownum id from dual connect by level <= 1e4 ) select trunc(dbms_random.value(1,1001)) n1 from generator v1, generator v2 where rownum <= 3e5 ; create or replace function func_normal(i_in number) return number as begin return round(exp(ln(i_in))); end; /
That’s 300,000 rows in the table and a silly little function to use up some CPU to get nowhere. There are 1,000 distinct integer values scattered uniformly through the table, and the function returns the value it’s called with – but it does it a hard way.
Here’s some test code – with “set timing on” – followed by the results:
select count(distinct(n1)) from t1; select count(distinct(func_normal(n1))) from t1 ; COUNT(DISTINCT(N1)) ------------------- 1000 1 row selected. Elapsed: 00:00:00.03 COUNT(DISTINCT(FUNC_NORMAL(N1))) -------------------------------- 1000 1 row selected. Elapsed: 00:00:11.39
This is running on 12.1.0.2 on a machine with a CPU speed of aboaut 3 GHz – you may want to adjust the number of rows in the table for your own testing.
The question is, what options can we use to improve the efficiency of the calls to the PL/SQL. One option, of course, is to use the new 12c “with function” clause – we could embed the function in the SQL like this:
with function func_with ( i_in number ) return number is begin return round(exp(ln(i_in))); end; select count(distinct(func_with(n1))) from t1 / COUNT(DISTINCT(FUNC_WITH(N1))) ------------------------------ 1000 1 row selected. Elapsed: 00:00:09.77
So, for the cost of copying the function into the SQL we get a 10% improvement in performance – which we could reasonably attribute to an improved efficiency in the call mechanism. There are arguments for and against copying code like this, of course, and my improvement was only 1 second for 300,000 calls, but you may decide that the benefit of the “With function” method is sufficient to justify the effort.
If you don’t want to copy a PL/SQL function into the SQL, though, there is another alternative – the pragma UDF (for user-defined function) – which has the effect of reducing the cost of the so-called “context switch” as you call from SQL to PL/SQL or vice versa.
Technically I think what’s happening is that the stack formats for SQL and PL/SQL are different and the context switch is the work needed to reformat the stack as you pass from one environment to the other – by declaring the function as UDF you probably set it up to deal with the incoming SQL stack itself. [But see comment 8 below, linking to an article that uses a debugger to check the effects pragma UDF]
Here’s how to add the UDF pragma to the function, and the results showing the effects:
create or replace function func_udf(i_in number) return number as pragma UDF; begin return round(exp(ln(i_in))); end; / COUNT(DISTINCT(FUNC_UDF(N1))) ----------------------------- 1000 1 row selected. Elapsed: 00:00:09.55
With pragma UDF the standalone function call is slightly faster than the in-line “with function”. I did wonder whether the pragma UDF would make the function call slower if I simply called it from a loop in a PL/SQL block, but there didn’t seem to be any significant difference between the normal function and the UDF function.
Initially, then, it looks like UDF is faster than “with function”, which is faster than basic; but there are other considerations. My sample data has only 1,000 possible inputs to the function – and Oracle has three different methods for caching that I might be able to benefit from:
- Declaring the function as deterministic
- Putting the function into the PL/SQL result cache
- Modifying the SQL to take advantage of scalar subquery caching
Here’s what the function declaration looks like if I want to use the PL/SQL function cache:
create or replace function func_cached(i_in number) return number result_cache as begin return round(exp(ln(i_in))); end; /
Changing my query to use func_cached() the query completed in 1.65 seconds – a clear winner, but can anything else get close.
To make the functions deterministic, I just have to add the word “deterministic” after the declaration of the return type:
create or replace function func_normal(i_in number) return number deterministic as begin return round(exp(ln(i_in))); end; /
We can also add the deterministic keyword to the function defined in the “with” clause. Before reporting the results of testing the functions with determinism, there’s one more strategy to consider. Remove the deterministic key word from the functions, and introduce a scalar subquery to the test query, e.g.:
select count(distinct(select func_normal(n1) from dual)) from t1 ;
Here’s a table of results:
Method | Initial | Deterministic | Scalar Subquery |
Basic | 11.39 | 4.30 | 4.32 |
With | 9.77 | 9.72 | 3.60 |
UDF | 9.55 | 9.57 | 3.57 |
Cached | 1.65 | 0.71 | 0.72 |
Summary
Before stating any conclusions it’s worth remembering that the sample data is based on a relatively small number of distinct input values. It is the repetition that allows us to benefit from things like caching and determinism. On top of that we need to consider the scale of the time-saving in light of the number of calls made (or not made).
Having said that, the PL/SQL function cache is clearly the thing that gives us the most benefit in return for a simple implementation. We should remember that the cache is “public” – i.e. stored in the SGA – and each input value takes up another bit of the public result cache: on the plus side this means that everyone else calling this function gets the benefit of our prior calculation; on the minus side this means if we use the result cache for the wrong function then we could take up a lot of space in the cache for very little benefit. Since the relevant result cache latch has not child latches it’s also possible to end up with latch contention if the too many sessions are taking advantage of the result cache too frequently.
Although we might expect a deterministic function to give us a big benefit (in the “very repetitive inputs” case), we find that the deterministic keyword has no effect (as at 12.1.0.2) in functions declared in a WITH clause or declared as standalone with pragma UDF. Fortunately scalar subquery caching (which seems to use the same hashing algorithm as the deterministic caching algorithm) still works with WITH functions or UDF functions and (slightly surprisingly) standalone functions declared with pragma UDF seem to have a very small edge over WITH functions.
Both of the latter two approaches use a local cache with a strictly limited size. The benefit is that the locality means they won’t interfere with other sessions or end up hogging an extreme amount of a public memory area; the drawback is that the size limit (which can be adjusted with a hidden parameter) means that you can get unlucky with hash collisions and end up with extremely variable performance from day to day because of a small change in the data being processed, or even a change in the order in which an unchanged data set is processed.
Footnote
I’ll leave you with one thought. Without declaring the original function as deterministic, I ran the following query to maximise the benefit from the scalar subquery caching algorithm:
select count(distinct(select func_normal(n1) from dual)) from ( select /*+ no_eliminate_oby */ n1 from t1 order by n1 ) ;
The query completed in 0.13 seconds: the subquery was called 1,000 times (once for each distinct value – see this ancient URL), and the benefit of eliminating the function calls outweighed the cost of having to sort the data set.
Update (May 2019)
With the upgrade to 12.2 the deterministic keyword started working for the inline “with function”, and some change to the coding for pragma UDF also appeared so that both strategies produced performance similar to the scalar subquery improvements.
Update Nov 2022
Repeating the tests on 19.11.0.0 on a machine with a nominal CPU speed of 2.5GHz I got the following results:
Method | Initial | Deterministic | Scalar Subquery |
Basic | 7.71 | 2.88 | 3.03 |
With | 7.34 | 2.71 | 2.81 |
UDF | 7.27 | 2.70 | 2.78 |
Cached | 0.42 | 0.26 | 0.35 |
The results agree with the observation made in the May 2019 update – for this test at least the deterministic pragma seems to have brought the performance of “with” functions and the UDF pragma up to the same level as it does for stored functions; but PL/SQL function caching still wins by a comfortable margin.
I’d say also that performance of inline-functions depends on the number of “PL/SQL Virtual Machine started”/PL/SQL Virtual Machine stopped”(the number of optimized operations), we can see it with 10938 trace: http://orasql.org/2013/07/13/oracle-12c-behavior-tests-of-the-inline-functions-identities-and-defaults/
Comment by Sayan Malakshinov — October 9, 2015 @ 6:29 pm BST Oct 9,2015 |
And I think there is big difference between one function call in select-list and with several calls in select/where/having clauses.
Comment by Sayan Malakshinov — October 9, 2015 @ 6:30 pm BST Oct 9,2015 |
One more option – DIY session-level cache (it’s session-level so many statements can benefit, can be used in standard edition [unlike result cache], and you can control the cache size).
A simple example:
Thanks,
Oren.
Comment by Oren Nakdimon (@DBoriented) — October 9, 2015 @ 6:54 pm BST Oct 9,2015 |
Hi Jonathan – thank you for wonderful example. I have a doubt –
Is the function above same as below?
Because it sounds something is not right – You are not using DETERMINISTIC clause – so how is ORACLE calling function only 1000 times? For example if you had done wouldn’t it result in much more calls than 1000 (it will be unknown number of rows to be frank) (following wont qualify for deterministic any way due to SYSDATE call)
I can understand following will lead to 1000 iterations –
Am i misunderstanding something?
Thank you
/S/
Comment by ora777 — October 9, 2015 @ 6:58 pm BST Oct 9,2015 |
/S/
The function is the same, and the reason is because of the way that scalar subquery caching works. There’s a detailed explanation in this article: https://jonathanlewis.wordpress.com/2006/11/06/filter-subqueries/ which I’ve now linked to in the main posting.
Comment by Jonathan Lewis — October 9, 2015 @ 8:17 pm BST Oct 9,2015 |
Hi Jonathan, I am really concerned about the “scalar subquery” thing – At the end of the day if I input 5 rows and if the function is NOT deterministic – I had assumed function will be called 5 times. All optimizer tricks aside. Because I may be doing something complex in the function that ORACLE couldn’t possibly underestimate – for example if I said heartbeat of patient on 1st minute of every hour for last 24 hours. So input is 1 for 24 rows – does not mean heartbeat is same. Here is example – I feel the sub-query is throwing some curve on my understanding. On any other day I would have said answer would be same – but now you have shown that its not the case. We need to agree on one of the ANSWER is right and one is wrong – I am not talking about path taken. If I try to explain to heart patient that they have no problem because subquery says so – wont fly – would it? (I am sorry I could come up with better analogy if more time spent..)
/S/
Comment by ora777 — October 9, 2015 @ 9:01 pm BST Oct 9,2015 |
You missed one basic point here. It’s was recently added to the oracle docs by Tom Kyte:
“Because SQL is a declarative language, rather than an imperative (or procedural) one, you cannot know how many times a function invoked by a SQL statement will run—even if the function is written in PL/SQL, an imperative language.
If your application requires that a function be executed a certain number of times, do not invoke that function from a SQL statement. Use a cursor instead”
https://docs.oracle.com/database/121/ADFNS/adfns_packages.htm#ADFNS00908
So you must NOT rely on your function to be called a specific number of times when used in a sql statement.
Think about what perhaps might happen in Oracle DB 19z. May be the optimizer then is smart enough to recognize that your function is nothing more than select sysdate from … .
You wont expect different sysdates from select sysdate from all_objects by now, will you?
Comment by chris227 — October 14, 2015 @ 10:57 am BST Oct 14,2015 |
Chris227,
I didn’t respond to your previous post because there didn’t seem to be a question involved. All I could see was that you were worried about the implications of scalar subquery caching. It happens, that’s a fact; if you want to argue about whether or not it should happen then you have to raise an SR with Oracle.
As far as your follow-up post is concerned, you might like to consider that I’ve written an article that says “the number of times the function will be called can vary”, and Tom Kyte’s addition to the documentation says “you cannot know how many times the function will be called”. I’ve shown a couple of strategies for things that might minimise the number of times a function could be called – Tom Kyte has written something about handling a requirement where the exact number of calls has to take place.
In fact, Tom has given you a solution to your requirement for calling a function many times with the same input, the right number of times and in the right order, and getting a correct but constantly varying result on each call.
Your comment about 19z carries some weight, of course – whenever you come up with a “trick” that manages to take advantage of some unannounced optimisation you have to be very careful that the trick still works on the next release; that’s why documentation and reproducible test cases are so very important. One of the side effects of unannounced optimisations that “disappear”, though is that sometimes you don’t even realise that your SQL has taken advantage of them until something slows down in the upgrade, so it’s always good to test important code very carefully, especially when it runs faster than you were expecting.
Comment by Jonathan Lewis — October 15, 2015 @ 10:05 am BST Oct 15,2015 |
Hi Jonathan,
chris227’s follow up was a replay to ora77 comment.
Not more than a naiv attempt of predection of what you might have answered.
So it was ora77 missing the point mentioned not you ;-)
Comment by chris227 — October 16, 2015 @ 10:16 am BST Oct 16,2015
Chris227
Sorry about the mix-up.
I hope it’s still possible for people to make sense of the various comments.
Comment by Jonathan Lewis — October 16, 2015 @ 2:09 pm BST Oct 16,2015
For ora777:
I have been under the impression, that scalar subquery happens only inside one execution of the statement. When statement ends, then cache is flushed
Comment by nemo — October 12, 2015 @ 1:52 pm BST Oct 12,2015 |
Nemo,
Thanks for the comment – the best (most current) reference for the effects and differences between the effects of deterministic caching and scalar subquery caching is probably the set of articles written by Sayan Malakshinov. I created a link to the three articles in order a few months ago: https://jonathanlewis.wordpress.com/2015/02/08/functions-subqueries/
Comment by Jonathan Lewis — October 15, 2015 @ 9:37 am BST Oct 15,2015 |
[…] posted a note a few days ago about read consistency, the Cross Session PL/SQL Function Result Cache, deterministic functions, […]
Pingback by Read Consistency | Oracle Scratchpad — October 12, 2015 @ 7:44 pm BST Oct 12,2015 |
[…] esto viene a cuento de que en una entrada reciente de su blog, Jonathan Lewis analiza y compara el rendimiento de esas opciones en un caso de […]
Pingback by Funciones en SQL: opciones para la mejora de rendimiento | Database and More — November 22, 2015 @ 3:44 pm GMT Nov 22,2015 |
Hi,
One can check the internal behavior of the “PRAGMA UDF;” mechanism with PERF and GDB on Linux: http://blog.ora-600.pl/2015/10/29/oracle-12c-pragma-udf-the-truth/
Regards!
Kamil.
Comment by Kamil Stawiarski — November 24, 2015 @ 8:02 am GMT Nov 24,2015 |
Kamil,
That’s interesting – thanks for posting the link.
Comment by Jonathan Lewis — November 24, 2015 @ 9:23 am GMT Nov 24,2015 |