Pythian Blog: Technical Track

Select vs Assign – How To Assign PL/SQL Variables

Select vs Assign

It isn’t unusual to see PL/SQL that contains select columns into variables from some_table

This is normal and expected when the data you need is in a table.

However, many years ago, Oracle provided a workaround for using SELECT statements in a somewhat unorthodox way via the DUAL table.

Some examples:

select sysdate into vDate from dual;

select user into vUser from dual;

select sys_context('userenv','sid') into vSID from dual;

While such usage can be useful at times, it’s a bit of a performance hog.

In general, do not use select .. into when you could use a direct variable assignment instead.

Here a PL/SQL block is using select into to assign a date to a variable:

declare
  vDate date;
begin
  select sysdate into vDate from dual;
end;
/

The same assignment can be performed directly:

declare
  vDate date;
begin
  vDate := sysdate;
end;
/

Do you think it makes any difference which method is used?

If you think they’re equivalent, you may want to keep reading.

Testing

We’ll use two SQL scripts: select.sql and assign.sql.

Each script will assign sysdate to vDate 1M times in a loop.

We’ll use two different forms of monitoring for the tests:

  • perf
  • Oracle Trace

Additionally, we’ll run the scripts without any monitoring, just so we can see the timing data.

We’ll set timing via set timing on in SQLPlus.

If you’re unfamiliar with perf, this is a good place to start: perf. I show all scripts in their entirety at the end of this article.

Timings

Before doing any kind of tracing, I’ll first run the test scripts to get timing information.

First I’ll run select.sql, then assign.sql.

Each script will make 1M variable assignments.

select.sql

SQL# @select
USERNAME                    SID SPID
-------------------- ---------- ------------------------
JKSTILL                      51 13280

1 row selected.

Testing speed of 'select into var'
Press ENTER when ready

Working...

PL/SQL procedure successfully completed.
Elapsed: 00:00:07.99

Assigning sysdate into vDate 1M times took 7.99 seconds.

assign.sql

SQL# @assign
USERNAME                    SID SPID
-------------------- ---------- ------------------------
JKSTILL                      51 13280

1 row selected.
Elapsed: 00:00:00.02

Testing speed of 'var := something'
Press ENTER when ready

Working...

PL/SQL procedure successfully completed.
Elapsed: 00:00:00.33

Directly assigning via vDate := sysdate 1M times took significantly less time at 0.33 seconds.

It should be clear that you should never use select from dual when you’re able to use a direct variable assignment.

Now, let’s dig a little deeper and get a better understanding of why there’s such a large difference between the two methods of assigning a value to a variable.

Testing with Perf

We can use perf to count the operations performed by the server.

The record.sh script is used to start the recording on the server.

Testing Method

This is a fairly simple manual testing method.

Each of the SQL scripts will pause until I press ENTER.

While the SQL script is paused, I switch to the ssh session where I’m logged into the database server as root.

Then I start the perf recording via ./record.sh PID

Switching back to the SQLPlus session, I press ENTER.

Then I switch back to the server, and press CTL-C when the SQLPlus job is done.

Though somewhat crude, this method is sufficient for these tests.

I performed each test in this way, resulting in two files:

  • data.assign
  • data.select

These files were renamed from the default perf.data following each test.

In the previous tests you may have noticed that the SPID was reported. This refers to the server PID for the Oracle process started on behalf of the SQLPlus session. It’s this PID that’s used with the record.sh script.

./record.sh 13280

I ran the same SQL scripts while recording each with perf.

perf report

Now to create a report from each file.

I used the following command to create a nice execution tree of each data file, along with counts for each function called.

perf report --stdio -g count -i perf.data.select

Although the output is fairly interesting, we won’t be delving into it today. What’s most interesting at this time is the number of operations performed, expressed as counters in perf.

We aren’t looking at timing, just how much work had to be done on the server for each test script.

For that, we just need one line from each file:

$ grep 'Event count'  perf.rpt.*
 perf.rpt.assign:# Event count (approx.): 223223223
 perf.rpt.select:# Event count (approx.): 7292292285

Well, that’s interesting. The number of calls when running the select.sql script is 32x that of the assign.sql script.

Testing with Oracle Trace

There are a number of methods to start a trace on an Oracle Session.

Here I’ll be using the old standby, alter session set events '10046 trace name context forever, level 12' simply because I have a script for it, and the name is easy to remember.

I once again ran the same two test SQL scripts, but this time by first setting the tracefile_identifier and enabling the trace.

Running select.sql with 10046 trace

alter session set tracefile_identifier = 'SELECT';
select value from v$diag where name = 'Default Trace File';
@@10046
@@select
exit

Running assign.sql with 10046 trace

alter session set tracefile_identifier = 'ASSIGN';
select value from v$diag where name = 'Default Trace File';
@@10046
@@assign
exit

Then I copied the tracefiles from the server.

Analysis

We can learn a bit just by checking the sizes of the files:

$  wc cdb1_ora*.trc
  3000405   6001742 256119114 cdb1_ora_5689_SELECT.trc
      159       850      9088 cdb1_ora_6414_ASSIGN.trc
  3000564   6002592 256128202 total

There is a striking disparity in the size of those files. The overhead of using Oracle Trace caused the execution time of select.sql to balloon from eight seconds to 55 seconds.

Here’s a simple profile of each trace file:

$ ./profiler-2.pl cdb1_ora_5689_SELECT.trc
Response Time Component                    Duration     Pct    # Calls      Dur/Call
----------------------------------------  ---------  ------  ---------  ------------
CPU service                                  45.13s   80.9%         12     3.761055s
SQL*Net message from client                   6.12s   11.0%          7     0.874465s
unaccounted-for                               4.52s    8.1%          1     4.515813s
library cache lock                            0.00s    0.0%          1     0.000958s
library cache pin                             0.00s    0.0%          1     0.000514s
PGA memory operation                          0.00s    0.0%         29     0.000009s
SQL*Net message to client                     0.00s    0.0%          7     0.000001s
----------------------------------------  ---------  ------  ---------  ------------
Total response time                          55.77s  100.0%


$ ./profiler-2.pl cdb1_ora_6414_ASSIGN.trc
Response Time Component                    Duration     Pct    # Calls      Dur/Call
----------------------------------------  ---------  ------  ---------  ------------
SQL*Net message from client                   6.63s   95.5%          7     0.946822s
CPU service                                   0.31s    4.4%         12     0.025454s
unaccounted-for                               0.01s    0.1%          1     0.009777s
PGA memory operation                          0.00s    0.0%          2     0.000009s
SQL*Net message to client                     0.00s    0.0%          7     0.000001s
----------------------------------------  ---------  ------  ---------  ------------
Total response time                           6.94s  100.0%

It would seem the results are a bit skewed by the overhead of the trace, as there are 45 seconds of CPU used (recall that without tracing, the script took eight seconds).

Using standard linux tools, we can get a better idea of why the select.sql takes so much more time than assign.sql.

select.sql trace

  awk '{ print $1 }' cdb1_ora_5689_SELECT.trc | sort | uniq -c | sort -n | tail -20
      3 select
      3 toid
      3 value=###
      3 value=4294951004
      5 =====================
      5 END
      5 PARSING
      7 PARSE
     11 kxsbbbfp=7f7826769da0
     12 STAT
     14 BINDS
     14 Bind#0
     14 oacdty=02
     14 oacflg=00
     45 WAIT
     58 ***
     67
1000013 FETCH
1000016 EXEC
1000021 CLOSE

assign.sql trace

awk '{ print $1 }' cdb1_ora_6414_ASSIGN.trc | sort | uniq -c | sort -n | tail -20
      3 BINDS
      3 Bind#0
      3 Bind#1
      3 Dump
      3 Dumping
      3 END
      3 PARSING
      3 oacdty=02
      3 oacdty=123
      3 oacflg=00
      3 oacflg=01
      3 toid
      3 value=###
      5 EXEC
      5 PARSE
      9
      9 STAT
     10 CLOSE
     11 ***
     16 WAIT

The last three lines of the report for select.sql tell the story; when assigning variable via select into from dual, Oracle had to create, fetch and close a cursor 1M times.

That overhead can be avoided simply by assigning variables directly, as seen in assign.sql.

Any blog that uses perf for analysis would be incomplete without the requisite flame graphs.

Flame graph for select.sql

What’s of interest in these flame graphs is the amount of work being done once the script enters the plsql_run section.

The select.sql script has quite a bit of code being executed; not only is there a large stack of code being executed, it is very wide, which in a flame graph indicates a lot of work being performed.

Flame graph for select.sql.

Flame graph for assign.sql

Now take a look at the flame graph for assign.sql.  There’s much less above the plsql_run section. Not only is the stack shorter, it’s much narrower, and therefore faster.

Flame graph for assign.sql.

Conclusion

It’s good to periodically test your assumptions.

You probably wouldn’t notice the difference in singleton events that happen too quickly for a human to perceive the difference in timing. But when scaled up, such as I have done here, the differences are easy to see.

Will using a direct assignment make a noticeable difference in a PL/SQL program that does it only once? Probably not. But what if that PL/SQL program is called frequently?

What if there are several PL/SQL programs doing this? Maybe some of them doing so in a loop?

Not only would the difference in performance be discernable, but this activity would consume extra resources; leaving them unavailable for other processes.

Over time, small changes can add up to significant performance improvements.

The next time you’re writing some PL/SQL, be sure to look at it with a critical eye toward the impact it will have on system performance.

Scripts

Below please find all the scripts I used:

get-curr-ospid.sql

-- get-curr-ospid.sql
--
-- get the server OS Pid for the current session
-- Jared Still jkstill@gmail.com still@pythian.com
col username format a20

select
    s.username,
    s.sid,
    p.spid
from v$session s, v$process p
where s.sid = sys_context('userenv','sid')
    and p.addr = s.paddr
order by username, sid
/

select.sql

@get-curr-ospid

prompt
prompt Testing speed of 'select into var'
prompt
prompt Press ENTER when ready
prompt

accept dummy
prompt Working...
prompt

set timing on

declare
    vDate date;
begin
    for i in 1..1e6
    loop
        select sysdate into vDate from dual;
    end loop;
end;
/

assign.sql

@get-curr-ospid

prompt
prompt Testing speed of 'var := something'
prompt
prompt Press ENTER when ready
prompt

accept dummy
prompt Working...
prompt

set timing on

declare
    vDate date;
begin
    for i in 1..1e6
    loop
        vDate := sysdate;
    end loop;
end;
/

record.sh

#!/usr/bin/env bash

declare PID=$1

: ${PID:?Please supply PID}

[[ "$PID" =~ ^[0-9]+$ ]] || {
    echo
    echo $PID is not numeric
    echo
    exit 1
}

echo Recording:

perf record -F 999 -T -g --timestamp-filename  -p $PID

10046.sql

-- level 4 is bind values
-- level 8 is waits
-- level 12 is both

--
-- see 10046_off.sql to end tracing

alter session set events '10046 trace name context forever, level 12';
--sys.dbms_system.set_ev(sid(n), serial(n), 10046, 8, '');

10046_off.sql

alter session set events '10046 trace name context off'
/

You can find all the relevant files on github: select-vs-assign.

Thank you for reading and please feel free to leave any thoughts or questions in the comments.

No Comments Yet

Let us know what you think

Subscribe by email