How to Use FORALL for Bulk DML in PL/SQL

Replace slow row-by-row loops with FORALL, which sends one DML statement for a whole collection, and see how much faster it is.

Inserting or updating rows one at a time in a PL/SQL loop switches between the PL/SQL and SQL engines once per row. FORALL sends one DML statement to SQL with a whole collection of values, which does the work of thousands of rows in a single switch. It is the DML counterpart of BULK COLLECT, and often many times faster.

Code for This Guide

The main examples are in the examples/dynamic-bulk folder of the Oracle Database 26ai code repository on GitHub, each with its output. They use NIMBUS, the sample schema of a fictional airline, which you can install with the scripts in the setup/nimbus folder.

They come from Oracle Database 26ai SQL and PL/SQL Book.

Syntax

forall i in {low .. high | indices of collection | values of index_collection}
  [save exceptions]
  dml_statement;

The index can only subscript collections in the statement: compute any other values into collections first. SQL%ROWCOUNT gives the total rows affected and SQL%BULK_ROWCOUNT(i) the rows of each execution.

The examples write into a small work table:

Setup:

create table fare_snapshot (route_id number, fare number);

Insert and Update a Collection at Once

Example:

declare
  type t_ids   is table of number;
  v_ids   t_ids;
  v_fares t_ids := t_ids();
begin
  select route_id bulk collect into v_ids from routes where origin = 'DXB';
  v_fares.extend(v_ids.count);
  for i in 1 .. v_ids.count loop
    v_fares(i) := 100 + i;          -- FORALL can only subscript collections: prepare values
  end loop;

  forall i in 1 .. v_ids.count
    insert into fare_snapshot (route_id, fare) values (v_ids(i), v_fares(i));
  dbms_output.put_line(sql%rowcount || ' rows inserted in one call');

  forall i in 1 .. 3
    update fare_snapshot set fare = fare * 2 where route_id = v_ids(i);
  for i in 1 .. 3 loop
    dbms_output.put_line('route ' || v_ids(i) || ': '
                         || sql%bulk_rowcount(i) || ' updated');
  end loop;
  rollback;
end;
/

Output:

20 rows inserted in one call
route 35: 1 updated
route 7: 1 updated
route 25: 1 updated

PL/SQL procedure successfully completed.

One FORALL inserts the 20 routes from Dubai in a single call. The second updates three routes, and SQL%BULK_ROWCOUNT reports one row for each. The block rolls back.

Sparse Collections with INDICES OF

FORALL i IN 1 .. n requires every element from 1 to n to exist. INDICES OF uses only the indexes that exist.

Example:

declare
  type t_numbers is table of number index by pls_integer;
  v_routes t_numbers;
  v_fares  t_numbers;
begin
  v_routes(1) := 1;  v_fares(1) := 100;              -- a sparse collection: indexes 1, 5, 9
  v_routes(5) := 5;  v_fares(5) := 500;
  v_routes(9) := 9;  v_fares(9) := 900;
  forall i in indices of v_fares
    insert into fare_snapshot values (v_routes(i), v_fares(i));
  dbms_output.put_line(sql%rowcount || ' rows from a sparse collection');
  rollback;
end;
/

Output:

3 rows from a sparse collection

PL/SQL procedure successfully completed.

The example drops the work table afterward.

How Much Faster

Copying the 11,224 crew assignments row by row and then with FORALL shows the difference.

Example:

declare
  type t_crew is table of crew_assignments%rowtype;
  v_crew  t_crew;
  v_start timestamp;
  function ms_since (p_start timestamp) return number is
  begin
    return round(extract(second from (localtimestamp - p_start)) * 1000);
  end;
begin
  select * bulk collect into v_crew from crew_assignments;

  v_start := localtimestamp;
  for i in 1 .. v_crew.count loop
    insert into crew_copy values v_crew(i);                -- one statement per row
  end loop;
  dbms_output.put_line(v_crew.count || ' rows, row by row: ' || ms_since(v_start) || ' ms');
  rollback;

  v_start := localtimestamp;
  forall i in 1 .. v_crew.count
    insert into crew_copy values v_crew(i);                -- one statement for all rows
  dbms_output.put_line(v_crew.count || ' rows, FORALL:     ' || ms_since(v_start) || ' ms');
  rollback;
end;
/

Output:

11224 rows, row by row: 54 ms
11224 rows, FORALL:     3 ms

PL/SQL procedure successfully completed.

The loop takes many times longer than FORALL, and the gap grows with the number of rows. Your times will differ; the ratio will not.

Things to Know

  • FORALL holds one DML statement; for several statements, write several FORALL loops.
  • For millions of rows, combine BULK COLLECT ... LIMIT and FORALL in batches to bound memory.
  • By default the first failing row stops FORALL; SAVE EXCEPTIONS processes the rest and reports failures afterward.

Related Guides

Conclusion

FORALL runs one DML statement for a whole collection in a single call to SQL, far faster than a row-by-row loop. Prepare values in collections first, use INDICES OF for sparse collections, check SQL%BULK_ROWCOUNT per row, and batch very large sets with BULK COLLECT LIMIT.

Vinish Kapoor
Vinish Kapoor

An Oracle ACE, author of four books on Oracle APEX, SQL and PL/SQL, and Oracle Forms, and a software developer building Oracle database applications since 2001.

guest

0 Comments
Oldest
Newest Most Voted