The RETURNING INTO clause is one of my favorite PL/SQL features. It allows to write less code, improves readability and reduces context switches between PL/SQL and SQL.
In this post I’d like to highlight some less-known characteristics of the RETURNING INTO clause and emphasize differences that exist when it is used in different DML statements.
Supported Statements
The RETURNING INTO clause is supported by the UPDATE, DELETE, and single-table single-row (“values-based”) INSERT statements.
It is not supported by subquery-based INSERT, multi-table INSERT, and MERGE. Some additional restrictions are documented.
What is returned?
The RETURNING INTO clause returns data from the rows that have been affected by the DML statement.
For INSERT it returns the after-insert values of the new row’s columns.
For UPDATE it returns the after-update values of the affected rows’ columns.
For DELETE it returns the before-delete values of the affected rows’ columns.
> create table t ( id number generated as identity, data number ); Table created. > declare ret1 number; ret2 number; begin insert into t (data) values (10) returning id,data into ret1,ret2; dbms_output.put_line('id='||ret1||' ; data='||ret2); insert into t (data) values (20) returning id,data into ret1,ret2; dbms_output.put_line('id='||ret1||' ; data='||ret2); insert into t (data) values (30) returning id,data into ret1,ret2; dbms_output.put_line('id='||ret1||' ; data='||ret2); commit; end; / id=1 ; data=10 id=2 ; data=20 id=3 ; data=30 PL/SQL procedure successfully completed.
> declare ret number; begin update t set data=111 where id=1 returning data into ret; dbms_output.put_line('post-update value = '||ret); delete t where id=2 returning data into ret; dbms_output.put_line('pre-delete value = '||ret); rollback; end; / post-update value = 111 pre-delete value = 20 PL/SQL procedure successfully completed.
Expressions
The returned values are not limited just to simple column names. It can virtually be any expression you can place in the select-list of a SELECT statement from the affected table.
> declare
ret1 number;
ret2 varchar2(10);
begin
update t
set data = round(dbms_random.value(1,100))
where id = 1
returning data, decode(mod(data,2),0,'even','odd')
into ret1, ret2;
dbms_output.put_line('the new value is '||ret1||', an '||ret2||' number');
rollback;
end;
/
the new value is 19, an odd number
PL/SQL procedure successfully completed.
Warning: you can even include in the returned expression a scalar subquery against the affected table. This is probably not a very good idea, especially with INSERT and UPDATE, which return the post-DML values. The scalar subquery, on the other hand, obeys the statement-level read consistency rule – it returns the data as it was before the DML happened. So it may lead to confusing inconsistent results:
> declare
ret1 number;
ret2 number;
begin
update t
set data = 29081969
where id = 1
returning data, (select max(data) from t)
into ret1, ret2;
dbms_output.put_line('just set DATA of a row to ' || ret1 ||
' and the maximum value is (no! *was*) ' || ret2);
rollback;
end;
/
just set DATA of a row to 29081969 and the maximum value is (no! *was*) 30
PL/SQL procedure successfully completed.
BULK COLLECT
The returned values can be placed into collection variables, using RETURNING with the BULK COLLECT INTO clause. This is very useful for the UPDATE and DELETE statements, that may affect multiple rows in a single statement (but this syntax is supported also for the INSERT statement – perhaps as a preparation for supporting RETURNING in multi-row INSERT in the future?).
> declare
type ntt is table of number;
ret1 ntt;
ret2 ntt;
begin
update t
set data = data * 2
returning id, data
bulk collect into ret1, ret2;
for i in 1..ret1.count loop
dbms_output.put_line('id='||ret1(i)||' ; data='||ret2(i));
end loop;
rollback;
end;
/
id=1 ; data=20
id=2 ; data=40
id=3 ; data=60
PL/SQL procedure successfully completed.
Aggregation
For UPDATE and DELETE the returned expression can be an aggregate function. The aggregation is performed on the affected rows, and (since the aggregated value is scalar) BULK COLLECT is not needed when using this option. This is not supported for INSERT.
> declare
ret1 number;
ret2 number;
ret3 varchar2(100);
begin
update t
set data = data * 2
where id < 3
returning sum(data),
avg(data),
listagg(id||':'||data,',') within group (order by id)
into ret1, ret2, ret3;
dbms_output.put_line('sum(data) of updated rows = ' || ret1);
dbms_output.put_line('avg(data) of updated rows = ' || ret2);
dbms_output.put_line('new values = ' || ret3);
rollback;
end;
/
sum(data) of updated rows = 60
avg(data) of updated rows = 30
new values = 1:20,2:40
PL/SQL procedure successfully completed.
RETURNING or RETURN?
One last anecdote: the RETURN keyword can be used instead of RETURNING. I prefer RETURNING.
What about MERGE?
As many others, I wish the MERGE statement would support the RETURNING INTO clause as well.
However, since MERGE is a combination of INSERT, UPDATE and DELETE, and given the differences in the way RETURNING works with these 3 DML statements (as listed above), supporting RETURNING INTO in MERGE is clearly not a simple task.
It may also lead to confusing situations – think for example about a MERGE statement that includes the UPDATE SET and DELETE WHERE clauses – what will RETURNING return for rows affected by the DELETE WHERE clause? The pre-MERGE value, the post-UPDATE-pre-DELETE value, or perhaps simply 42?
Excellent summary !
Thanks.
marvelous
You wrorte “but this syntax is supported also for the INSERT statement – perhaps as a preparation for supporting RETURNING in multi-row INSERT in the future” in the “BULK COLLECT” section, but it can already be used now with FORALL:
———————————————————————
declare
type ntt is table of number;
type vtt is table of varchar2(100);
val ntt;
ret vtt;
begin
val := ntt( 10, 20, 30);
forall i# in indices of val
insert into t (data) values (val(i#))
returning ‘id=’||id||’ ; data=’||data
bulk collect into ret;
for i in 1..ret.count loop
dbms_output.put_line(ret(i));
end loop;
rollback;
end;
/
———————————————————————
The result will be (on the second run):
———————————————————————
id=4 ; data=10
id=5 ; data=20
id=6 ; data=30
———————————————————————
Thanks for your comment Niels.
I still hope that RETURNING (BULK COLLECT) INTO will be supported one day for subquery-based INSERTs.
Thanks,
Oren.