As specified by Steven Feuerstein into the Oracle Blog Website the bulk processing features of PL/SQL are designed specifically to reduce the number of context switches required to communicate from the PL/SQL engine to the SQL engine.

Using BULK COLLECT plus FORALL instead of standard Insert statement to insert data improve performance dramatically, let’s me show you :

Here is a customer case on how using BULK COLLECT plus FORALL to improve Insert operations for very big tables (more than 1 billion of rows and more than 300Gb in size).

First of all, create a bigfile tablespace which will contain the data:

SQL> CREATE bigfile TABLESPACE tbs1 DATAFILE '+data' SIZE 310G;

Tablespace created.

Elapsed: 00:11:30.87

Next step is to create the table (empty) and move it to the bigfile tablespace:

CREATE TABLE "xxxx"."DBI_FK_NOPART"
( "PKEY" NUMBER(12,0) NOT NULL ENABLE,
"BOID" VARCHAR2(40 CHAR) COLLATE "USING_NLS_COMP" NOT NULL ENABLE,
"METABO" NUMBER(12,0) NOT NULL ENABLE,
"LASTUPDATE" TIMESTAMP (9) NOT NULL ENABLE,
"PROCESSID" VARCHAR2(40 CHAR) COLLATE "USING_NLS_COMP" NOT NULL ENABLE,
"ROWCOMMENT" VARCHAR2(15 CHAR) COLLATE "USING_NLS_COMP",
"CREATED" TIMESTAMP (9) NOT NULL ENABLE,
"CREATEDUSER" VARCHAR2(40 CHAR) COLLATE "USING_NLS_COMP" NOT NULL ENABLE,
"REPLACED" TIMESTAMP (9) NOT NULL ENABLE,
"REPLACEDUSER" VARCHAR2(40 CHAR) COLLATE "USING_NLS_COMP",
"ARCHIVETAG" VARCHAR2(40 CHAR) COLLATE "USING_NLS_COMP",
"MDBID" VARCHAR2(255 CHAR) COLLATE "USING_NLS_COMP",
"ITSFORECAST" VARCHAR2(40 CHAR) COLLATE "USING_NLS_COMP" NOT NULL ENABLE,
"BETRAG" NUMBER(15,2) NOT NULL ENABLE,
"ITSOPDETHERKUNFT" VARCHAR2(40 CHAR) COLLATE "USING_NLS_COMP",
"ITSOPDETHKERSTPRM" VARCHAR2(40 CHAR) COLLATE "USING_NLS_COMP",
"ITSFCKOMPPREISSEQ" VARCHAR2(40 CHAR) COLLATE "USING_NLS_COMP",
"CLSFCKOMPPREISSEQ" NUMBER(12,0),
"ISSUMMANDENDPREIS" NUMBER(12,0) NOT NULL ENABLE,
"PARTITIONTAG" NUMBER(12,0) NOT NULL ENABLE,
"PARTITIONDOMAIN" VARCHAR2(4 CHAR) COLLATE "USING_NLS_COMP" NOT NULL ENABLE,
"FCVPRODKOMPPKEY" NUMBER(12,0),
"FCKVPRDANKOMPPKEY" NUMBER(12,0)
) ;

--MOVE TABLE TO BIGFILE TABLESPACE
ALTER TABLE "xxxx"."DBI_FK_NOPART" MOVE ONLINE TABLESPACE tbs1;

Load Data with BULK COLLECT and FORALL:

SQL> declare
type testarray is table of varchar2(3000) index by binary_integer;
v_PKEY testarray;
v_BOID testarray;
v_METABO testarray;
v_LASTUPDATE testarray;
v_PROCESSID testarray;
v_ROWCOMMENT testarray;
v_CREATED testarray;
v_CREATEDUSER testarray;
v_REPLACED testarray;
v_REPLACEDUSER testarray;
v_ARCHIVETAG testarray;
v_MDBID testarray;
v_ITSFORECAST testarray;
v_BETRAG testarray;
v_ITSOPDETHERKUNFT testarray;
v_ITSOPDETHKERSTPRM testarray;
v_ITSFCKOMPPREISSEQ testarray;
v_CLSFCKOMPPREISSEQ testarray;
v_ISSUMMANDENDPREIS testarray;
v_PARTITIONTAG testarray;
v_PARTITIONDOMAIN testarray;
v_FCVPRODKOMPPKEY testarray;
v_FCKVPRDANKOMPPKEY testarray;

cursor cu_cursor is select PKEY,BOID,METABO,LASTUPDATE,PROCESSID,ROWCOMMENT,CREATED,CREATEDUSER,REPLACED,REPLACEDUSER,ARCHIVETAG,MDBID,ITSFORECAST,BETRAG,
ITSOPDETHERKUNFT,ITSOPDETHKERSTPRM,ITSFCKOMPPREISSEQ,CLSFCKOMPPREISSEQ,ISSUMMANDENDPREIS,PARTITIONTAG,PARTITIONDOMAIN,FCVPRODKOMPPKEY,FCKVPRDANKOMPPKEY
FROM xxx.TableSource;

begin
dbms_output.put_line('start : '||to_char(sysdate,'dd.mm.rrrr hh24:mi:ss'));
open cu_cursor;

loop

fetch cu_cursor bulk collect into v_PKEY,v_BOID,v_METABO,v_LASTUPDATE,v_PROCESSID,v_ROWCOMMENT,v_CREATED,v_CREATEDUSER,v_REPLACED,v_REPLACEDUSER ,v_ARCHIVETAG,v_MDBID,v_ITSFORECAST,v_BETRAG,v_ITSOPDETHERKUNFT,
v_ITSOPDETHKERSTPRM ,v_ITSFCKOMPPREISSEQ ,v_CLSFCKOMPPREISSEQ,v_ISSUMMANDENDPREIS ,v_PARTITIONTAG,v_PARTITIONDOMAIN,v_FCVPRODKOMPPKEY,v_FCKVPRDANKOMPPKEY LIMIT 1000;

forall i in 1 .. v_PKEY.count

insert into xxx.DBI_FK_NOPART( PKEY,BOID,METABO,LASTUPDATE,PROCESSID,ROWCOMMENT,CREATED,CREATEDUSER,REPLACED,REPLACEDUSER ,ARCHIVETAG,MDBID,ITSFORECAST,BETRAG,ITSOPDETHERKUNFT,
ITSOPDETHKERSTPRM ,ITSFCKOMPPREISSEQ ,CLSFCKOMPPREISSEQ,ISSUMMANDENDPREIS ,PARTITIONTAG,PARTITIONDOMAIN,FCVPRODKOMPPKEY,FCKVPRDANKOMPPKEY )
values
( v_PKEY(i),v_BOID(i),v_METABO(i),v_LASTUPDATE(i),v_PROCESSID(i),v_ROWCOMMENT(i),v_CREATED(i),v_CREATEDUSER(i),v_REPLACED(i),v_REPLACEDUSER(i),v_ARCHIVETAG(i),v_MDBID(i),v_ITSFORECAST(i),v_BETRAG(i),v_ITSOPDETHERKUNFT(i),
v_ITSOPDETHKERSTPRM(i),v_ITSFCKOMPPREISSEQ(i),v_CLSFCKOMPPREISSEQ(i),v_ISSUMMANDENDPREIS(i),v_PARTITIONTAG(i),v_PARTITIONDOMAIN(i),v_FCVPRODKOMPPKEY(i),v_FCKVPRDANKOMPPKEY(i));

exit when cu_cursor%notfound;
end loop;

close cu_cursor;
dbms_output.put_line('end : '||to_char(sysdate,'dd.mm.rrrr hh24:mi:ss'));

end;
/
start : 15.11.2021 10:30:36
end : 15.11.2021 12:50:23

PL/SQL procedure successfully completed.

Elapsed: 02:19:46.80

Gather Statistics:

exec dbms_stats.gather_table_stats('xxx','DBI_FK_NOPART');

Add primary key and indexes (store it into bigfile tablespace) :

ALTER TABLE xxx.DBI_FK_NOPART ADD CONSTRAINT PK6951_1 PRIMARY KEY (PKEY) using index tablespace tbs1;

BEGIN
CREATE INDEX "xxx"."CLSFCKOMPPREISSEQ695_1" ON "xxx"."DBI_FK_NOPART" ("CLSFCKOMPPREISSEQ") TABLESPACE "TBS1";
CREATE INDEX "xxx"."ITSFCKOMPPREISSEQ695_1" ON "xxx"."DBI_FK_NOPART" ("ITSFCKOMPPREISSEQ") TABLESPACE "TBS1";
CREATE INDEX "xxx"."ITSFORECAST695_1" ON "xxx"."DBI_FK_NOPART" ("ITSFORECAST") TABLESPACE "TBS1" ;
CREATE INDEX "xxx"."IX_MDBID_xxx_1" ON "xxx"."DBI_FK_NOPART" ("MDBID") TABLESPACE "TBS1" ;
END;

Let’s check statistics of the table :

select owner,table_name,num_rows,blocks, last_analyzed from dba_tables where table_name = 'DBI_FK_NOPART';
OWNER TABLE_NAME      NUM_ROWS  BLOCKS    LAST_ANALYZED
XXX    DBI_FK_NOPART  1188403800 39871915 15.11.21

Conclusion :

With BULK COLLECT plus FORALL, I inserted more than 1 billion of rows in 02h19.

With standard Insert through a FOR LOOP statement, the Insert never finished, I stopped it after 15 hours of execution and after resizing muliple times the Undo tablespace due to “unable to extend tablespace…” error.