All Products
Search
Document Center

PolarDB:Special usage

Last Updated:Mar 28, 2026

When PL/SQL processes large result sets row by row, repeated context-switching between the database engine and the PL/SQL runtime creates significant overhead. PolarDB for Oracle provides three bulk-processing features to eliminate this overhead: BULK COLLECT INTO fetches multiple rows into a collection in a single operation, FORALL sends multiple DML statements to the engine in one round trip, and record-based INSERT/UPDATE syntax lets you write collection elements directly to a table without intermediate unpacking.

BULK COLLECT INTO

BULK COLLECT INTO can insert multiple rows of data from a table into a collection variable. Unlike the INTO clause — which works only with scalars or row variables — BULK COLLECT INTO targets collection variables and can populate them with multiple rows at once. Initialization is not required: even if the collection already contains elements, it is reinitialized automatically before the clause runs.

BULK COLLECT INTO can be used in three contexts.

SELECT ... BULK COLLECT INTO

The following example extracts all rows from test into a nested table variable and then inserts them into test_backup using FORALL:

-- Preparation
CREATE TABLE test (id INT, name VARCHAR(10));
INSERT INTO test VALUES(1, 'a'), (2, 'b');
CREATE TABLE test_backup (id INT, name VARCHAR(10));

DECLARE
  TYPE r_type IS RECORD(id INT, name VARCHAR(10));
  TYPE nt_type IS TABLE OF r_type;
  nt nt_type;
BEGIN
  SELECT * BULK COLLECT INTO nt FROM test;
  FORALL i IN INDICES OF nt
    INSERT INTO test_backup VALUES nt(i);
END;

SELECT * FROM test_backup;

Result:

 id | name
----+------
  1 | a
  2 | b
(2 rows)

FETCH cursor BULK COLLECT INTO

Use LIMIT to cap the number of rows fetched per call and avoid excessive memory use:

DECLARE
  TYPE nt_int IS TABLE OF test.id%TYPE;
  TYPE nt_char IS TABLE OF test.name%TYPE;
  nt1 nt_int;
  nt2 nt_char;
  CURSOR cur IS SELECT id, name FROM test;
BEGIN
  OPEN cur;

  -- Fetch at most 1 row per call to control memory usage.
  FETCH cur BULK COLLECT INTO nt1, nt2 LIMIT 1;
  FOR i IN 1..nt1.COUNT LOOP
    RAISE NOTICE '% %', nt1(i), nt2(i);
  END LOOP;

  CLOSE cur;
END;

Result:

NOTICE:  1 a
DO

RETURNING ... BULK COLLECT INTO

Use this form to capture the rows affected by a DELETE, INSERT, UPDATE, or EXECUTE IMMEDIATE statement:

DECLARE
  TYPE t_rowtype IS TABLE OF test%ROWTYPE INDEX BY PLS_INTEGER;
  r t_rowtype;
BEGIN
  UPDATE test SET id = id + 1 RETURNING * BULK COLLECT INTO r;
  FOR i IN r.FIRST..r.LAST LOOP
    RAISE NOTICE '%', r(i);
  END LOOP;
END;

Result:

NOTICE:  (2,a)
NOTICE:  (3,b)
DO

FORALL

FORALL is a bulk DML statement that sends multiple DML operations to the database engine in a single round trip. It is similar to FOR .. LOOP but has tighter restrictions: the loop body can contain only a single SQL or dynamic SQL statement.

FORALL supports three iteration forms:

-- Iterate over a numeric range.
FORALL i IN lower_bound..upper_bound
  single SQL

-- Iterate over the index keys of a collection
-- (only associative arrays with numeric indexes are supported).
FORALL i IN INDICES OF collection
  single SQL

-- Iterate over the element values of a collection
-- (only collections whose elements are of numeric type are supported).
FORALL i IN VALUES OF collection
  single SQL

Track rows affected per statement

SQL%BULK_ROWCOUNT is an associative array populated automatically after each FORALL run. Each entry holds the number of rows affected by the corresponding loop iteration.

The following example runs two iterations. The first iteration matches two rows (id = 1 appears twice in the table); the second matches one row:

-- Preparation
DELETE FROM test;
INSERT INTO test VALUES (1, 'a'), (1, 'b'), (2, 'c');

DECLARE
  TYPE nt_type IS TABLE OF INT;
  nt nt_type := nt_type(1, 2);
BEGIN
  FORALL i IN 1..2
    UPDATE test SET id = id + 10 WHERE id = nt(i);

  FOR i IN SQL%BULK_ROWCOUNT.FIRST..SQL%BULK_ROWCOUNT.LAST LOOP
    RAISE NOTICE '%', SQL%BULK_ROWCOUNT(i);
  END LOOP;
END;

Result:

NOTICE:  2
NOTICE:  1
DO

Handle exceptions with SAVE EXCEPTIONS

By default, FORALL raises an exception on the first failing statement and stops. To skip failed statements and continue the loop, add the SAVE EXCEPTIONS clause:

FORALL i IN lower_bound..upper_bound SAVE EXCEPTIONS
  single SQL
Important

SAVE EXCEPTIONS requires statement-level transactions to be enabled on your database. After all iterations complete, FORALL raises the FORALL_DML_ERROR exception.

When FORALL_DML_ERROR is raised, inspect SQL%BULK_EXCEPTIONS to get details on every failed statement. Each entry has three fields:

FieldDescription
ERROR_INDEXThe loop iteration number that failed
ERROR_CODEThe database error code for that failure
ERROR_MSGThe error message text

For example, if a FORALL SAVE EXCEPTIONS loop runs five iterations and the second and fourth fail, then SQL%BULK_EXCEPTIONS.COUNT equals 2, SQL%BULK_EXCEPTIONS(1).ERROR_INDEX is 2, and SQL%BULK_EXCEPTIONS(2).ERROR_INDEX is 4.

The following example inserts five values into a table with a primary key constraint. The second and fourth inserts duplicate existing keys and fail:

-- Make sure that statement-level transactions are enabled.

-- Create a table with a primary key constraint.
CREATE TABLE test_unique (id INT PRIMARY KEY);

DECLARE
  TYPE nt_int IS TABLE OF INT;
  nt nt_int := nt_int(1, 1, 2, 2, 3);
BEGIN
  -- The 2nd and 4th iterations fail because of duplicate primary key values.
  FORALL i IN 1..nt.COUNT SAVE EXCEPTIONS
    INSERT INTO test_unique VALUES(nt(i));

EXCEPTION
  WHEN FORALL_DML_ERROR THEN
    RAISE NOTICE 'SQLCODE: %, SQLERRM: %', SQLCODE, SQLERRM;

    FOR i IN 1..SQL%BULK_EXCEPTIONS.COUNT LOOP
      RAISE NOTICE 'no: %',        i;
      RAISE NOTICE 'err_index: %', SQL%BULK_EXCEPTIONS(i).ERROR_INDEX;
      RAISE NOTICE 'err_code: %',  SQL%BULK_EXCEPTIONS(i).ERROR_CODE;
      RAISE NOTICE 'err_msg: %',   SQL%BULK_EXCEPTIONS(i).ERROR_MSG;
      RAISE NOTICE '---------------';
    END LOOP;

END;

-- View the rows that were successfully inserted.
SELECT * FROM test_unique;

Result:

-- PL/SQL output
NOTICE:  SQLCODE: P0005, SQLERRM: save exceptions raised in FORALL statememt
NOTICE:  no: 1
NOTICE:  err_index: 2
NOTICE:  err_code: 23505
NOTICE:  err_msg: duplicate key value violates unique constraint "test_unique_pkey"
NOTICE:  ---------------
NOTICE:  no: 2
NOTICE:  err_index: 4
NOTICE:  err_code: 23505
NOTICE:  err_msg: duplicate key value violates unique constraint "test_unique_pkey"
NOTICE:  ---------------
DO

-- SELECT output
 id
----
  1
  2
  3
(3 rows)
Note

When iterating over a collection variable with FORALL or FOR, check whether the collection contains deleted placeholders. Accessing a deleted placeholder raises a runtime error.

INSERT and UPDATE with record types

PL/SQL supports inserting or updating a table row using a record variable directly:

-- Insert a record variable into a table.
INSERT INTO test VALUES record;

-- Update a table row using a record variable.
UPDATE table SET ROW = record;

The same syntax works when the source is a collection variable whose elements are of a record type.

Insert example — insert one element of a nested table into a table:

DELETE FROM test;

DECLARE
  TYPE r_type IS RECORD(id INT, name VARCHAR(10));
  TYPE nt_type IS TABLE OF r_type;
  nt nt_type := nt_type(r_type(25, 'y'));
BEGIN
  INSERT INTO test VALUES nt(1);
END;

SELECT * FROM test;

Result:

 id | name
----+------
 25 | y
(1 row)

Update example — update all rows in a table using an element of a nested table:

DECLARE
  TYPE r_type IS RECORD(id INT, name VARCHAR(10));
  TYPE nt_type IS TABLE OF r_type;
  nt nt_type := nt_type(r_type(26, 'z'));
BEGIN
  UPDATE test SET ROW = nt(1);
END;

SELECT * FROM test;

Result:

 id | name
----+------
 26 | z
(1 row)

The following example uses FORALL to bulk-update test_backup from a collection variable:

DECLARE
  TYPE r_type IS RECORD(id INT, name VARCHAR(10));
  TYPE nt_type IS TABLE OF r_type;
  nt nt_type := nt_type(r_type(1, 'c'), r_type(2, 'd'));

BEGIN
  FORALL i IN nt.FIRST..nt.LAST
    UPDATE test_backup SET ROW = nt(i) WHERE id = nt(i).id;
END;

SELECT * FROM test_backup;

Result:

 id | name
----+------
  1 | c
  2 | d
(2 rows)