PL/SQLintermediate
Bulk Operations: BULK COLLECT and FORALL
Explain how to optimize PL/SQL performance for large data volumes using bulk binding techniques
A PL/SQL loop issuing one SQL statement per row switches between the PL/SQL engine and the SQL engine on every iteration. That switch is the cost, and at volume it dominates everything else.
Row-by-row processing is like a waiter carrying one plate at a time from the kitchen to the dining room for a 200-guest banquet — hugely inefficient. BULK COLLECT and FORALL are like using a large catering cart to move dozens of plates in a single trip.
Key Concepts
1
The naive version.
FOR r IN (SELECT * FROM staging) LOOP
INSERT INTO target VALUES (r.a, r.b);
END LOOP;2
One hundred thousand rows means one hundred thousand context switches in each direction.
3
The bulk version.
DECLARE
TYPE t_tab IS TABLE OF staging%ROWTYPE;
l_data t_tab;
CURSOR c IS SELECT * FROM staging;
BEGIN
OPEN c;
LOOP
FETCH c BULK COLLECT INTO l_data LIMIT 1000;
EXIT WHEN l_data.COUNT = 0;4
FORALL i IN 1 .. l_data.COUNT
INSERT INTO target VALUES (l_data(i).a, l_data(i).b);5
END LOOP;
CLOSE c;
END;6
100,000 rows, row by row: 200,000 context switches
100,000 rows, LIMIT 1000: 200 context switches7
The individual inserts still happen — FORALL is not a single statement — but they are handed to the SQL
engine in one batch.
FORALL
8
LIMIT is not optional. BULK COLLECT without it fetches every row into session memory:
LIMITBULK COLLECT
9
ten million rows -> the whole result set in PGA -> the session
runs out of memory10
A limit of a few hundred to a few thousand gets nearly all the benefit and bounds the memory. Leaving it out
is the standard way this optimisation becomes an outage.
11
SAVE EXCEPTIONS keeps going after a failure.
FORALL i IN 1 .. l_data.COUNT SAVE EXCEPTIONS
INSERT INTO target VALUES (...);SAVE EXCEPTIONS
12
Without it, one bad row raises an exception and the whole batch stops. With it, the failures are collected
and examined afterwards:
13
EXCEPTION
WHEN bulk_errors THEN
FOR i IN 1 .. SQL%BULK_EXCEPTIONS.COUNT LOOP
l_idx := SQL%BULK_EXCEPTIONS(i).ERROR_INDEX;
l_err := SQL%BULK_EXCEPTIONS(i).ERROR_CODE;
END LOOP;14
ERROR_INDEX is the position within the batch, which is how you identify which source row failed — and the
reason you keep the collection around rather than discarding it.
ERROR_INDEX
15
Where this matters in EBS. Any custom program processing interface rows or applying corrections across
large transaction sets. Oracle's own import programs are written this way, which is a reasonable argument
for writing yours the same.
16
What the interviewer is probing.1. "Why are these faster?" Probing: the mechanism. Stalls: "Less code." Moves up: they cut
context switches between the PL/SQL and SQL engines — one switch for a batch instead of one per row.
17
2. "What is the memory risk with BULK COLLECT?" Probing: the trap. Stalls: "None." *Moves
up:* an unbounded collect loads the whole result set into session memory, so a LIMIT clause with a
loop is required on large sets.
18
3. "What does SAVE EXCEPTIONS do?" Probing: the error handling. Stalls: "Nothing." *Moves
up:* lets FORALL continue past failing rows and reports them in SQL%BULK_EXCEPTIONS, instead of
aborting the whole batch.
19
4. "What does SQL%BULK_ROWCOUNT give you?" Probing: the detail. Stalls: "The total." *Moves
up:* rows affected per iteration, which is how you verify each element did what you expected.