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.

Get more Vinish.dev tutorials on Google

See my articles more often when you search Google.

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