Oracle PL/SQL performance tuning using BULK COLLECT and FORALL

Here I’ll show you, in Oracle database, how to do performance tuning of your pl/sql code using BULK COLLECT and FORALL. Using BULK COLLECT and FORALL instead of normal FOR LOOP I have achieved significant performance benefit. In some cases I was able to tune the performance from 14 minutes to just 10 seconds.

You may already know that in Oracle database the PL/SQL code is executed by PL/SQL engine and SQL is executed by SQL engine. Also you know that SQL is embedded with PL/SQL. When PL/SQL engine encounters SQL code, it passes control to SQL engine for execution. This is called context switching between SQL and PL/SQL.

For example see the code below:

FOR i in 1..1000 LOOP
insert into emp values (…);
END LOOP;

In the above code when PL/SQL engine is executing the for loop, it needs to execute the INSERT statement 1000 times. This also means there were 1000 context switching. This generally degrades performance and causes longer execution time. Instead of context switching 1000 times, by using BULK COLLECT and FORALL method, the same operation can be achieved with just a single context switch. So that is where you can achieve significant performance benefit.

The test case here copies 1,000,000 records from a simple source table to a destination table. The source and destination tables have same structure. The only difference is that the source table has a primary key.

Here is the code that I used to create the source table called t_source and the destination table called t_dest:

create table t_source(
empno number,
ename varchar2(10),
joindate date);

alter table t_source add constraint pk_src primary key (empno);

create table t_dest as select * from t_source;

Then I created 1,000,000 records in t_source using this simple code

declare
begin
for i in 1..1000000 loop
insert into t_source values (i, ‘emp’||i, sysdate);
end loop;
commit;
end;
/

After inserting source data, I analyzed the table so that the optimizer has the latest statistics about the source table. That is to help Oracle as much as possible to find out the best way to get the data from t_source table. This may be not necessary for a simple table such as this. But depending on the number of rows and number of columns, it may help enormously.

Anyway here is the code to gather table stats.

EXEC DBMS_STATS.gather_table_stats(‘JOE’, ‘T_SOURCE’);

Then lets set the SQL environment so that we can see SQL execution time. Run this from SQL prompt:

SET TIMING ON;

Now I am going to populate t_dest using the usual FOR LOOP which is a very common method used by programmers. The code basically gets data into a cursor and then read each record from the cursor and inserts into t_dest table.

declare
cursor c1 is
select * from t_source;
begin
for src_rec in c1 loop
insert into t_dest values (src_rec.empno, src_rec.ename, src_rec.joindate);
end loop;
commit;
end;

Once the execution is complete you will get the output something like this.

PL/SQL procedure successfully completed.

Elapsed: 00:02:40.12
SQL>

Depending on the speed of your machine and database parameters, your code may take more time than you can see here or it may take less time. We are going to need this timing to compare against code that uses BULK COLLECT and FORALL.

Now run this code from your SQL*PLUS session

truncate table t_dest;

declare
cursor c1 is
select * from t_source;
TYPE src_tab IS TABLE OF t_source%ROWTYPE INDEX BY BINARY_INTEGER;
rec_tab src_tab;

begin
open c1;
fetch c1 BULK COLLECT INTO rec_tab limit 10000;
WHILE rec_tab.COUNT > 0 LOOP
FORALL i IN 1..rec_tab.COUNT
INSERT INTO t_dest (empno, ename, joindate) VALUES (rec_tab(i).empno,rec_tab(i).ename,rec_tab(i).joindate);
fetch c1 BULK COLLECT INTO rec_tab limit 10000;
END LOOP;
CLOSE c1;
end;

The output should shoething like this:

PL/SQL procedure successfully completed.

Elapsed: 00:00:16.13
SQL>

As you can see the time to insert 1,000,000 records from source to destination has decreased from 00:02:40.12 to just 00:00:16.13. This is a huge performance gain. When dealing with millions of records the performance benefit may be tremendous.

Using oracle hanganalyze tool of oradebug utility to analyze oracle hang

Sometime you may find that your SQL session is hanging or some application that accesses your database is hanging. The application that access an Oracle database basically creates a session to the database. And it is that session that will be hanging which to normal usr looks like the application is hanging.

Whether it is the SQL session/statement or an application that is hanging sometime it becomes difficult to find out what is causing the database to hang.

You may use the usual tools such as AWR (automatic workload repository) and ADDM (automatic database diagnostic monitor) to find out what is causing the database to hang. But many time the information you receive from this tool is not enough to determine what is causing it or to identify which is causing it.

Moreover if your database instance is hanging then you can not run AWR and ADDM.

In such case you can use a tool called hanganalyze. This tool is provided by oracle and is part of oradebug utility. This is very handy to find out exactly which session is causing the hang situation.

Here are the steps how you to you oracle hanganalyze tool:

Preparation:
Login as sysdba and run the following:

sqlplus / as sysdba
create user joe identified by joe;
grant create session to joe;
grant resource to joe;

Simulate a session hang (in this case row locking):
Login as user joe from two sessions. Lets refer them session 1 and session 2.

From session 1 do the following:

create table dept (
deptno number,
dname varchar2(20),
location varchar2(20));

insert into dept values (10, ‘Finance’,’London’);
insert into dept values (20, ‘HR’,’London’);

commit;

SQL> select * from dept;

DEPTNO DNAME LOCATION
———- ——————– ——————–
10 Finance London
20 HR London

update dept
set location=’New York’
where deptno = 10;

Now from session 2 try to update the same data by executing the code below:

update dept
set location=’Singapore’
where deptno = 10;

You will notice that the session is hanging. It is because the first update in session 1 has updated the same tow and the operation is not yet committed. So provide data consistency session 1 has locked the row and hence now session 2 needs to wait until session 1 commits or rollback;

Now run hanganalyze to find out exactly which session is blocking whom.
Login as sys user and find out the spid of the session that is hanged.

sqlplus / as sysdba

select a.sid, a.serial#, b.spid ospid, to_char(logon_time,’dd-Mon-rr hh24:mi’) Logintime
from gv$session a, gv$process b
where a.inst_id = b.inst_id and a.paddr = b.addr and status = ‘ACTIVE’;

Note down the ospid of the hanged session. Then run the following:

SQL> oradebug setospid [spid]
SQL> oradebug unlimit;
SQL> oradebug hanganalyze 3

This will generate a trace file which will contain the detail why the oracle database session is hanging. Even though the trace file will contain loads of information still it is quite easy to find out the real culprit.

Open the trace file and find out the session marked as “Chains most likely to have caused the hang:”. There you will see Chain 1 and Chain 2 which are basically session operations.
Going further down where these Chains are defined, you will see information such as session, serial#, blocking session, current SQL etc.

Soem extract of my trace file is as below:

Chains most likely to have caused the hang:
[a] Chain 1 Signature: ‘SQL*Net message from client’
Chain 1 Signature Hash: 0x38c48850
[b] Chain 2 Signature: ‘LNS ASYNC end of log’
Chain 2 Signature Hash: 0x8ceed34f

and section that provide chain detail

Chain 1:
——————————————————————————-
Oracle session identified by:
{
instance: 1 (dtcnmh.dtcnmh)
os id: 2653
process id: 36, oracle@SERV01A (TNS V1-V3)
session id: 770
session serial #: 11739
}
is waiting for ‘enq: TX – row lock contention’ with wait info:
{
p1: ‘name|mode’=0×54580006
p2: ‘usn
p3: ‘sequence’=0x507
time in wait: 59.699862 sec
timeout after: never
wait id: 26
blocking: 0 sessions
current sql: update dept
set location=’Singapore’

I hope this helps.