Skip to Main Content
  • Questions
  • How to force Oracle to run a subquery only once

Breadcrumb

Question and Answer

Connor McDonald

Thanks for the question, Stephen.

Asked: March 20, 2018 - 3:21 pm UTC

Last updated: September 30, 2022 - 4:32 am UTC

Version: 11.2.0.3

Viewed 10K+ times! This question is

You Asked

Hi,
I have some behaviour that I don't understand. I have built a call to a custom function in to a sql statement. The function returns a string that I want to use in an insert statement. The string is the concatenation of two bits of information e.g.
0023D89006044013D349510790BFA71873AE2CB7508E95D2D9B2B9EC35162F467C12
is 4 character identifier 0023 followed by a second string. I want to use the two parts of the returned string to insert to two different columns.

The call to the custom function does not return predictable results, so if called twice with the same inputs it will return different output strings.

Here is my problem SQL slimmed down to illustrate:
this select :
SELECT con41enc, con41enc FROM
(SELECT
CUSTOM_FUNC(CONX.ATTRIB_41) as con41enc
FROM
SIEBEL.S_CONTACT CON
,siebel.s_contact_x conx
WHERE
CON.row_id = '1-M3X5L'
and con.row_id = conx.par_row_id);

returns two different strings for con41enc and con41enc_1 e.g.
100214E1623F9FA7A88CE7C08044CD77410462E06C3A2F0DD87F3C18AFC90723A1FCE
100295C0EC0768E0EDEB4FC88F84614A831A01FA4F30C93ABD23181C2B7D56392D375


So the custom function has been called twice.

However, if I add rownum to the inner query

SELECT con41enc, con41enc FROM
(SELECT
rownum as fred
CUSTOM_FUNC(CONX.ATTRIB_41) as con41enc
FROM
SIEBEL.S_CONTACT CON
,siebel.s_contact_x conx
WHERE
CON.row_id = '1-M3X5L'
and con.row_id = conx.par_row_id);

I get the same string back twice
1002386FAF9DCE3DC59290D9E67ABE689639164F76356177342526E850CBDCB43CE1D
1002386FAF9DCE3DC59290D9E67ABE689639164F76356177342526E850CBDCB43CE1D


So adding rownum is stopping the query calling CUSTOM_FUNC twice, this is the required behaviour but I don't understand what is going on, if this is a robust solution and if not if there is another way to achieve my outcome.

Thanks in advance


and Chris said...

First up: if you're passing the same inputs to a function yet getting different results it's non-deterministic. If this is not what you expect, you need to look at its implementation. And fix that.

If the function contains a SQL query, this could be the reason you're getting different output:

https://blogs.oracle.com/sql/the-problem-with-sql-calling-plsql-calling-sql

But on to your question.

You can ensure the database executes a subquery once by materializing it. Adding rownum to it is one method. Another is to use the (undocumented) materialize hint.

For example, if you have a (rather daft) query with two scalar subqueries counting the rows in a table like so:

select (select count(*) from t) c1,
       (select count(*) from t) c2
from   dual;


The database accesses all the rows from T twice. As you can see from the following plan:

create table t as 
  select level x from dual
  connect by level <= 10000;
  
set serveroutput off

select /*+ gather_plan_statistics */
       (select count(*) from t) c1,
       (select count(*) from t) c2
from   dual;

select * 
from   table(dbms_xplan.display_cursor(null, null, 'ROWSTATS LAST'));

PLAN_TABLE_OUTPUT                                                       
SQL_ID  dv8jn3j2uxhta, child number 0                                   
-------------------------------------                                   
select /*+ gather_plan_statistics */        (select count(*) from t)    
c1,        (select count(*) from t) c2 from   dual                      
                                                                        
Plan hash value: 4268760765                                             
                                                                        
--------------------------------------------------------------          
| Id  | Operation          | Name | Starts | E-Rows | A-Rows |          
--------------------------------------------------------------          
|   0 | SELECT STATEMENT   |      |      1 |        |      1 |          
|   1 |  SORT AGGREGATE    |      |      1 |      1 |      1 |          
|   2 |   TABLE ACCESS FULL| T    |      1 |  10000 |  10000 |          
|   3 |  SORT AGGREGATE    |      |      1 |      1 |      1 |          
|   4 |   TABLE ACCESS FULL| T    |      1 |  10000 |  10000 |          
|   5 |  FAST DUAL         |      |      1 |      1 |      1 |          
--------------------------------------------------------------


Notice the 10,000 A-rows? To avoid this, place the count in a with subquery + materialize. It will then store the results of this in an on-the-fly temp table. And get the results from that.

You can see this from the two full scans of SYS_TEMP_0FD9D664F_51BBBA0 that return 1 row in the following query:

with rws as (
  select /*+ materialize */count(*) c from t
)
select /*+ gather_plan_statistics */
       (select c from rws) c1,
       (select c from rws) c2
from   dual;

select * 
from   table(dbms_xplan.display_cursor(null, null, 'ROWSTATS LAST'));

PLAN_TABLE_OUTPUT                                                                                            
SQL_ID  bwpbudsa74kjt, child number 0                                                                        
-------------------------------------                                                                        
with rws as (   select /*+ materialize */count(*) c from t ) select /*+                                      
gather_plan_statistics */        (select c from rws) c1,        (select                                      
c from rws) c2 from   dual                                                                                   
                                                                                                             
Plan hash value: 2197313032                                                                                  
                                                                                                             
----------------------------------------------------------------------------------------------------------   
| Id  | Operation                                | Name                       | Starts | E-Rows | A-Rows |   
----------------------------------------------------------------------------------------------------------   
|   0 | SELECT STATEMENT                         |                            |      1 |        |      1 |   
|   1 |  VIEW                                    |                            |      1 |      1 |      1 |   
|   2 |   TABLE ACCESS FULL                      | SYS_TEMP_0FD9D664F_51BBBA0 |      1 |      1 |      1 |   
|   3 |  VIEW                                    |                            |      1 |      1 |      1 |   
|   4 |   TABLE ACCESS FULL                      | SYS_TEMP_0FD9D664F_51BBBA0 |      1 |      1 |      1 |   
|   5 |  TEMP TABLE TRANSFORMATION               |                            |      1 |        |      1 |   
|   6 |   LOAD AS SELECT (CURSOR DURATION MEMORY)| SYS_TEMP_0FD9D664F_51BBBA0 |      1 |        |      0 |   
|   7 |    SORT AGGREGATE                        |                            |      1 |      1 |      1 |   
|   8 |     TABLE ACCESS FULL                    | T                          |      1 |  10000 |  10000 |   
|   9 |   FAST DUAL                              |                            |      1 |      1 |      1 |   
----------------------------------------------------------------------------------------------------------


Though the optimizer can spot many calls to the same subquery like this. So it can do the materialization above without the hint:

with rws as (
  select count(*) c from t
)
select /*+ gather_plan_statistics */
       (select c from rws) c1,
       (select c from rws) c2
from   dual;

select * 
from   table(dbms_xplan.display_cursor(null, null, 'ROWSTATS LAST'));

PLAN_TABLE_OUTPUT                                                                                            
SQL_ID  fsxss92494wva, child number 0                                                                        
-------------------------------------                                                                        
with rws as (   select count(*) c from t ) select /*+                                                        
gather_plan_statistics */        (select c from rws) c1,        (select                                      
c from rws) c2 from   dual                                                                                   
                                                                                                             
Plan hash value: 2197313032                                                                                  
                                                                                                             
----------------------------------------------------------------------------------------------------------   
| Id  | Operation                                | Name                       | Starts | E-Rows | A-Rows |   
----------------------------------------------------------------------------------------------------------   
|   0 | SELECT STATEMENT                         |                            |      1 |        |      1 |   
|   1 |  VIEW                                    |                            |      1 |      1 |      1 |   
|   2 |   TABLE ACCESS FULL                      | SYS_TEMP_0FD9D6650_51BBBA0 |      1 |      1 |      1 |   
|   3 |  VIEW                                    |                            |      1 |      1 |      1 |   
|   4 |   TABLE ACCESS FULL                      | SYS_TEMP_0FD9D6650_51BBBA0 |      1 |      1 |      1 |   
|   5 |  TEMP TABLE TRANSFORMATION               |                            |      1 |        |      1 |   
|   6 |   LOAD AS SELECT (CURSOR DURATION MEMORY)| SYS_TEMP_0FD9D6650_51BBBA0 |      1 |        |      0 |   
|   7 |    SORT AGGREGATE                        |                            |      1 |      1 |      1 |   
|   8 |     TABLE ACCESS FULL                    | T                          |      1 |  10000 |  10000 |   
|   9 |   FAST DUAL                              |                            |      1 |      1 |      1 |   
----------------------------------------------------------------------------------------------------------

Rating

  (6 ratings)

Is this answer out of date? If it is, please let us know via a Comment

Comments

materializing a sub-query

Stephen Palmer, March 20, 2018 - 4:26 pm UTC

fast and clear response, thanks!

Subquery (subselect) take much much longer

A reader, February 13, 2021 - 4:22 am UTC

Thanks for the great solution. The "with" to bring up the subquery/subselect to the top of the SQL statement as a forced temp result set, solved my problem that a small change to a subquery went from 3 seconds to many minutes because Oracle thought it needed to run the subquery (apparently) for every record so took forever.
Chris Saxon
February 15, 2021 - 3:27 pm UTC

Glad we could help

What about transaction Isolation level serializable or read only

Asim, September 28, 2022 - 10:52 pm UTC

Please refer to your blog you mentioned above and to all above discussion.

https://blogs.oracle.com/sql/the-problem-with-sql-calling-plsql-calling-sql

Also from oracle docs 21c

https://docs.oracle.com/cd/E25054_01/server.1111/e25789/consist.htm

Under the heading of read committed isolation level.
...
If a SELECT list contains a PL/SQL function, then the database applies statement-level read consistency at the statement level for SQL run within the PL/SQL function code, rather than at the parent SQL level. For example, a function could access a table whose data is changed and committed by another user. For each execution of the SELECT in the function, a new read-consistent snapshot is established.
....

But it says nothing about the read consistency of SQL run within PL sql called by SQL when the transaction isolation level is serializable or read only.

1
Can you please tell about the behavior of SQL inside PL SQL , when transaction isolation level is serializable or read only.

2.
If transaction isolation level is read committed or serializable, and the transaction first execute an insert then an update then, and then execute a SQL SELECT which calls PLSQL function, will the Select in the function will be able to see changes of the prior inserts and update?


Chris Saxon
September 29, 2022 - 10:11 am UTC

When you use serializable, all queries give results consistent to the start of the transaction plus any changes made within the transaction itself. This applies to SQL in PL/SQL.

For example, create an empty table, a function to count its rows, load some data in - leave these UNCOMMITTED!

create table t (
  c1 int
);

create or replace function f 
  return int as
  retval int;
begin
  dbms_session.sleep ( 10 );
  select count(*) into retval from t;
  return retval;
end f;
/

insert into t 
with rws as (
  select level x from dual
  connect by level <= 1000
)
  select * from rws;


(The sleep in the function is to give me time to switch between sessions & make it easier to view concurrency effects!)

In a different session, set the transaction level, insert a row, and call the function in a query. I've also set the query to count the table's rows to make it easier to see consistency effects.

While the query runs, commit the 1,000-row insert in the other session.

Using read committed the results are:

set transaction isolation level read committed;

insert into t values ( 0 );

select f, ( select count(*) from t ) from dual;

         F (SELECTCOUNT(*)FROMT)
---------- ---------------------
      1001                     1


So both queries see the insert in the session. The function sees the insert from the other session, but the plain SQL doesn't.

With serializable:

set transaction isolation level serializable;

insert into t values ( 0 );

select f, ( select count(*) from t ) from dual;

         F (SELECTCOUNT(*)FROMT)
---------- ---------------------
         1                     1


Both see the insert from the current session, but not the other session.

With read only:

set transaction read only;

insert into t values ( 0 );

ORA-01456: may not perform insert/delete/update operation inside a READ ONLY transaction

select f, ( select count(*) from t ) from dual;

         F (SELECTCOUNT(*)FROMT)
---------- ---------------------
         0                     0


The insert in the transaction fails (because it's read-only!) and neither query sees the data from the other session.

Once you complete the serializable & read only transactions (via commit or rollback), they can view the insert from the other session.

What about inline udf

Asim, September 29, 2022 - 3:22 pm UTC

What about the case when that pl sql function which is executing SQL inside it, is not a pre created object, but defined inline, i.e defined using WITH Function f1() in the main SELECT; this feature is available since 12c. ie

WITH FUNCTION f1 ()
BEGIN SELECT....END;
SELECT col1, f1() From t1;

Here I think in all transaction isolation levels, as the function defination is part of main SELECT, the SQL inside the function and the main SELECT must see the same state of the database ie. the data will be of same point of time for both parent/main and in-function SQL.



Connor McDonald
September 30, 2022 - 4:32 am UTC

Nope

SQL> with
  2  function xx return timestamp is
  3  begin
  4  return systimestamp;
  5  end;
  6  select systimestamp, xx() from scott.emp;
  7
  8  /

SYSTIMESTAMP                                                                XX()
--------------------------------------------------------------------------- -----------------------------------
30-SEP-22 12.31.39.609000 PM +08:00                                         30-SEP-22 12.31.39.609000000 PM
30-SEP-22 12.31.39.609000 PM +08:00                                         30-SEP-22 12.31.39.616000000 PM
30-SEP-22 12.31.39.609000 PM +08:00                                         30-SEP-22 12.31.39.616000000 PM
30-SEP-22 12.31.39.609000 PM +08:00                                         30-SEP-22 12.31.39.616000000 PM
30-SEP-22 12.31.39.609000 PM +08:00                                         30-SEP-22 12.31.39.616000000 PM
30-SEP-22 12.31.39.609000 PM +08:00                                         30-SEP-22 12.31.39.617000000 PM
30-SEP-22 12.31.39.609000 PM +08:00                                         30-SEP-22 12.31.39.617000000 PM
30-SEP-22 12.31.39.609000 PM +08:00                                         30-SEP-22 12.31.39.618000000 PM
30-SEP-22 12.31.39.609000 PM +08:00                                         30-SEP-22 12.31.39.618000000 PM
30-SEP-22 12.31.39.609000 PM +08:00                                         30-SEP-22 12.31.39.619000000 PM
30-SEP-22 12.31.39.609000 PM +08:00                                         30-SEP-22 12.31.39.619000000 PM
30-SEP-22 12.31.39.609000 PM +08:00                                         30-SEP-22 12.31.39.627000000 PM
30-SEP-22 12.31.39.609000 PM +08:00                                         30-SEP-22 12.31.39.627000000 PM
30-SEP-22 12.31.39.609000 PM +08:00                                         30-SEP-22 12.31.39.630000000 PM

14 rows selected.

Also dynamic query

Asim, September 29, 2022 - 4:25 pm UTC

Also, what if the SELECT inside pl sql function is
dynamic query executed with EXECUTE IMMEDIATE?
Connor McDonald
September 30, 2022 - 4:32 am UTC

Nope - see inline function demo

Systinestamp

Asim, October 04, 2022 - 3:44 pm UTC


example of systimestamp in PL SQL is not good here as,
Even if the transaction is set to serializeable and two different SQLs in the same session not in PLSQL, execute one after another, and selects systimestamp , will give different timestamp values and that will be of the time when each statement began, not of the time when transaction began.

More to Explore

SQL

The Oracle documentation contains a complete SQL reference.