Oracle PL/SQL Performance Tuning: Mastering BULK COLLECT & FORALL

Boost Oracle PL/SQL performance with Bulk Processing. Learn how BULK COLLECT and FORALL speed up data retrieval and DML operations.

Boost Oracle PL/SQL performance with Bulk Processing. Learn how BULK COLLECT and FORALL speed up data retrieval and DML operations.

Oracle PL/SQL Performance: BULK COLLECT and FORALL (Updated Guide)

When PL/SQL executes SQL row-by-row, Oracle performs many context switches between the PL/SQL and SQL engines.
For large datasets, this can become a major bottleneck.

Two features help solve this:

  • BULK COLLECT: fetch many rows into a collection in one operation.
  • FORALL: send many DML operations (INSERT/UPDATE/DELETE) in one operation.

Used together, they reduce context switches and can significantly improve throughput.


1) Test Data Setup

CREATE TABLE bulk_collect_test AS
SELECT owner, object_name, object_id
FROM all_objects;

## Oracle PL/SQL Performance: BULK COLLECT and FORALL (Updated Guide) When PL/SQL executes SQL row-by-row, Oracle performs many **context switches** between the PL/SQL and SQL engines. For large datasets, this can become a major bottleneck. Two features help solve this: - **BULK COLLECT**: fetch many rows into a collection in one operation. - **FORALL**: send many DML operations (INSERT/UPDATE/DELETE) in one operation. Used together, they reduce context switches and can significantly improve throughput. --- ## 1) Test Data Setup ```sql CREATE TABLE bulk_collect_test AS SELECT owner, object_name, object_id FROM all_objects; ``` --- ## 2) BULK COLLECT Basics ### Row-by-row fetch (slower pattern) ```sql SET SERVEROUTPUT ON DECLARE TYPE t_tab IS TABLE OF bulk_collect_test%ROWTYPE; l_tab t_tab := t_tab(); l_start NUMBER; BEGIN l_start := DBMS_UTILITY.get_time; FOR r IN ( SELECT owner, object_name, object_id FROM bulk_collect_test ) LOOP l_tab.EXTEND; l_tab(l_tab.LAST) := r; END LOOP; DBMS_OUTPUT.put_line('Row-by-row: ' || (DBMS_UTILITY.get_time - l_start)); END; / ``` ### BULK COLLECT fetch (faster pattern) ```sql SET SERVEROUTPUT ON DECLARE TYPE t_tab IS TABLE OF bulk_collect_test%ROWTYPE; l_tab t_tab; l_start NUMBER; BEGIN l_start := DBMS_UTILITY.get_time; SELECT owner, object_name, object_id BULK COLLECT INTO l_tab FROM bulk_collect_test; DBMS_OUTPUT.put_line('Bulk collect: ' || (DBMS_UTILITY.get_time - l_start)); END; / ``` > In most environments, BULK COLLECT is much faster for large result sets. --- ## 3) Use LIMIT to Control Memory Fetching everything at once can consume too much PGA memory. For large data, use chunking: ```sql SET SERVEROUTPUT ON DECLARE TYPE t_tab IS TABLE OF bulk_collect_test%ROWTYPE; l_tab t_tab; CURSOR c_data IS SELECT owner, object_name, object_id FROM bulk_collect_test; BEGIN OPEN c_data; LOOP FETCH c_data BULK COLLECT INTO l_tab LIMIT 1000; EXIT WHEN l_tab.COUNT = 0; -- Process l_tab here DBMS_OUTPUT.put_line('Chunk size: ' || l_tab.COUNT); END LOOP; CLOSE c_data; END; / ``` **Tip:** Start with LIMIT 500–5000 and benchmark. --- ## 4) FORALL for Bulk DML After loading data into collections, use FORALL for fast DML: ```sql DECLARE TYPE t_id_tab IS TABLE OF bulk_collect_test.object_id%TYPE; l_ids t_id_tab; BEGIN SELECT object_id BULK COLLECT INTO l_ids FROM bulk_collect_test WHERE object_id IS NOT NULL FETCH FIRST 10000 ROWS ONLY; FORALL i IN 1 .. l_ids.COUNT UPDATE bulk_collect_test SET object_name = object_name || '_X' WHERE object_id = l_ids(i); COMMIT; END; / ``` --- ## 5) Handle Partial Failures with SAVE EXCEPTIONS If one row fails, you may still want other rows processed: ```sql DECLARE TYPE t_id_tab IS TABLE OF bulk_collect_test.object_id%TYPE; l_ids t_id_tab; BEGIN SELECT object_id BULK COLLECT INTO l_ids FROM bulk_collect_test WHERE object_id IS NOT NULL FETCH FIRST 1000 ROWS ONLY; BEGIN FORALL i IN 1 .. l_ids.COUNT SAVE EXCEPTIONS DELETE FROM bulk_collect_test WHERE object_id = l_ids(i); EXCEPTION WHEN OTHERS THEN IF SQLCODE = -24381 THEN FOR j IN 1 .. SQL%BULK_EXCEPTIONS.COUNT LOOP DBMS_OUTPUT.put_line( 'Error at index ' || SQL%BULK_EXCEPTIONS(j).ERROR_INDEX || ', ORA-' || SQL%BULK_EXCEPTIONS(j).ERROR_CODE ); END LOOP; ELSE RAISE; END IF; END; COMMIT; END; / ``` --- ## 6) Practical Guidance - Prefer **set-based SQL** first (single SQL statement). - Use **BULK COLLECT + FORALL** when procedural logic is required. - Always benchmark: - row-by-row - BULK COLLECT with different LIMIT sizes - FORALL batch sizes - Avoid very large in-memory collections unless necessary. - Commit in reasonable batches for long-running jobs. --- ## 7) Oracle 10g+ Note Oracle can optimize some cursor FOR loops internally, but explicit bulk processing still gives you: - control over batch size, - predictable memory behavior, - better tuning for your specific workload. --- ## Conclusion For high-volume PL/SQL processing, BULK COLLECT and FORALL remain core performance tools. Use LIMIT, measure carefully, and tune batch sizes based on row width, memory budget, and DML complexity.
Subscribe
Notify of
guest