Oracle ACE Andrej Pashchenko recently did a blog post covering a nice addition in 23ai, the ability for MERGE to use the RETURNING clause.

Check his post for several examples, but as quick TL;DR, here is a simple example of the evolution.

My base will be all people in department 10 and 20 from the HR.EMPLOYEES
table.


SQL> create table emp_temp as
  2  select * from hr.employees
  3  where department_id in (10,20);

Table created.

and I will merge into it everyone from department 20 and 30 in HR.EMPLOYEES, so we will get some UPDATE activity (department 20) and INSERT activity
(department 30)


SQL> merge into emp_temp t
  2  using ( select * from hr.employees
  3          where department_id in (20,30) ) x
  4  on (t.employee_id = x.employee_id)
  5  when matched then
  6    update set t.salary = x.salary
  7  when not matched then
  8    insert values
  9      (x.employee_id,x.first_name
 10      ,x.last_name,x.email
 11      ,x.phone_number,x.hire_date
 12      ,x.job_id,x.salary
 13      ,x.commission_pct,x.manager_id
 14      ,x.department_id  );

Pre 23ai

Before 23ai, a MERGE works fine, but a MERGE-RETURNING bombs out with an error


SQL> select banner_full from v$version;

BANNER_FULL
----------------------------------------------------------------------------------------------------------------------------------
Oracle Database 21c Enterprise Edition Release 21.0.0.0.0 - Production
Version 21.16.0.0.0

SQL> merge into emp_temp t
  2  using ( select * from hr.employees
  3          where department_id in (20,30) ) x
  4  on (t.employee_id = x.employee_id)
  5  when matched then
  6    update set t.salary = x.salary
  7  when not matched then
  8    insert values
  9      (x.employee_id,x.first_name
 10      ,x.last_name,x.email
 11      ,x.phone_number,x.hire_date
 12      ,x.job_id,x.salary
 13      ,x.commission_pct,x.manager_id
 14      ,x.department_id  );

8 rows merged.

SQL>
SQL> roll;
Rollback complete.
SQL>
SQL> declare
  2    l_enums  sys.odcinumberlist;
  3    l_names  sys.odcivarchar2list;
  4  begin
  5    merge into emp_temp t
  6    using ( select * from hr.employees
  7            where department_id in (20,30) ) x
  8    on (t.employee_id = x.employee_id)
  9    when matched then
 10      update set t.salary = x.salary
 11    when not matched then
 12      insert values
 13        (x.employee_id,x.first_name
 14        ,x.last_name,x.email
 15        ,x.phone_number,x.hire_date
 16        ,x.job_id,x.salary
 17        ,x.commission_pct,x.manager_id
 18        ,x.department_id  )
 19    RETURNING employee_id, last_name
 20    bulk collect into l_enums, l_names;
 21  end;
 22  /
  RETURNING employee_id, last_name
            *
ERROR at line 19:
ORA-06550: line 19, column 13:
PL/SQL: ORA-00933: SQL command not properly ended
ORA-06550: line 5, column 3:
PL/SQL: SQL Statement ignored

23ai onwards

Now in 23ai, the RETURNING clause is allowed and we can get the rows returned by the operation.


SQL> set serverout on
SQL> declare
  2    l_enums  sys.odcinumberlist;
  3    l_names  sys.odcivarchar2list;
  4  begin
  5    merge into emp_temp t
  6    using ( select * from hr.employees
  7            where department_id in (20,30) ) x
  8    on (t.employee_id = x.employee_id)
  9    when matched then
 10      update set t.salary = x.salary
 11    when not matched then
 12      insert values
 13        (x.employee_id,x.first_name
 14        ,x.last_name,x.email
 15        ,x.phone_number,x.hire_date
 16        ,x.job_id,x.salary
 17        ,x.commission_pct,x.manager_id
 18        ,x.department_id  )
 19    returning employee_id, last_name
 20    bulk collect into l_enums, l_names;
 21
 22    for i in l_enums.first .. l_enums.last
 23    loop
 24      dbms_output.put_line('emp-'||l_enums(i)||':'||l_names(i));
 25    end loop;
 26  end;
 27  /
emp-201:Martinez
emp-202:Davis
emp-118:Himuro
emp-115:Khoo
emp-116:Baida
emp-119:Colmenares
emp-114:Li
emp-117:Tobias

PL/SQL procedure successfully completed.

But that got me thinking – maybe we can use this feature to boost other features.

Using new MERGE to boost other functionality.

You might think having a RETURNING clause is just a nice convenience, however it immediately opens up opportunities to fill in feature “gaps” that people have been asking for over the many versions of Oracle Database since 9i when MERGE was introduced.

For example, the RETURNING clause in MERGE also supports the extended NEW and OLD syntax that is also introduced in 23ai. In the case below I can pick up the old and new values for any primary key (EMPLOYEE_ID) values.


SQL> set serverout on
SQL> declare
  2    l_enums  sys.odcinumberlist;
  3    l_enums2  sys.odcivarchar2list;
  4  begin
  5    merge into emp_temp t
  6    using ( select * from hr.employees
  7            where department_id in (20,30) ) x
  8    on (t.employee_id = x.employee_id)
  9    when matched then
 10      update set t.salary = x.salary
 11    when not matched then
 12      insert values
 13        (x.employee_id,x.first_name
 14        ,x.last_name,x.email
 15        ,x.phone_number,x.hire_date
 16        ,x.job_id,x.salary
 17        ,x.commission_pct,x.manager_id
 18        ,x.department_id  )
 19    returning old employee_id, new employee_id
 20    bulk collect into l_enums, l_enums2;
 21
 22    for i in l_enums.first .. l_enums2.last
 23    loop
 24      dbms_output.put_line('emp-'||l_enums(i)||':'||l_enums2(i));
 25    end loop;
 26  end;
 27  /
emp-201:201
emp-202:202
emp-:118
emp-:115
emp-:116
emp-:119
emp-:114
emp-:117

PL/SQL procedure successfully completed.

Why is that useful? Once I have old and new values for the primary keys, I can deduce that

  • old and new present? It’s an update
  • Just new present? It’s an insert

By flipping my varray’s into nested tables to get the MULTISET functionality, I can now also use the values returned to come up with separate INSERT and UPDATE rowcounts, rather than just having SQL%ROWCOUNT.


SQL> set serverout on
SQL> declare
  2    type nt is table of number;
  3    l_enums  nt;
  4    l_enums2  nt;
  5    l_ins nt;
  6    l_upd nt;
  7  begin
  8    merge into emp_temp t
  9    using ( select * from hr.employees
 10            where department_id in (20,30) ) x
 11    on (t.employee_id = x.employee_id)
 12    when matched then
 13      update set t.salary = x.salary
 14    when not matched then
 15      insert values
 16        (x.employee_id,x.first_name
 17        ,x.last_name,x.email
 18        ,x.phone_number,x.hire_date
 19        ,x.job_id,x.salary
 20        ,x.commission_pct,x.manager_id
 21        ,x.department_id  )
 22    returning old employee_id, new employee_id
 23    bulk collect into l_enums, l_enums2;
 24
 25    l_upd := l_enums2 multiset intersect l_enums;
 26    dbms_output.put_line('update='||l_upd.count);
 27    l_ins := l_enums2 multiset except l_enums;
 28    dbms_output.put_line('insert='||l_ins.count);
 29
 30  end;
 31  /
update=2
insert=6

Now that MERGE has a RETURNING clause, you might be hopeful that INSERT-SELECT would also get a RETURNING extension but unfortunately not (yet).


SQL> declare
  2    l_enums  sys.odcinumberlist;
  3    l_names  sys.odcivarchar2list;
  4  begin
  5    insert into emp_temp t
  6    select * from hr.employees
  7    where department_id = 30
  8    returning employee_id, last_name
  9    bulk collect into l_enums, l_names;
 10
 11    for i in l_enums.first .. l_enums.last
 12    loop
 13      dbms_output.put_line('emp-'||l_enums(i)||':'||l_names(i));
 14    end loop;
 15  end;
 16  /
  returning employee_id, last_name
  *
ERROR at line 8:
ORA-06550: line 8, column 3:
PL/SQL: ORA-03049: SQL keyword 'RETURNING' is not syntactically valid following '...department_id in (20,30)
'
ORA-06550: line 5, column 3:
PL/SQL: SQL Statement ignored

However, since the WHEN MATCHED part of a MERGE command is optional, an INSERT command can (loosely) be re-phrased as an MERGE command. (There may be some differences between the the in the handling of duplicates here, but often you should be able to swap them out). For example, the INSERT above can be rewritten as a MERGE below where I have an always false condition which means every row will fall into the NOT MATCHED processing.


SQL> merge into emp_temp t
  2  using ( select * from hr.employees
  3          where department_id = 30 ) x
  4  on (1=0)
  5  when not matched then
  6    insert values
  7      (x.employee_id,x.first_name
  8      ,x.last_name,x.email
  9      ,x.phone_number,x.hire_date
 10      ,x.job_id,x.salary
 11      ,x.commission_pct,x.manager_id
 12      ,x.department_id  )
 13  /

6 rows merged.

At this point, from what we have seen above, I can add a RETURNING clause to the MERGE in order to pick up the rows I’ve inserted. However, unfortunately, PLSQL did not like my coding hack


SQL> roll;
Rollback complete.
SQL> declare
  2    l_enums  sys.odcinumberlist;
  3    l_names  sys.odcivarchar2list;
  4  begin
  5    merge into emp_temp t
  6    using ( select * from hr.employees
  7            where department_id = 30 ) x
  8    on (1=0)
  9    when not matched then
 10      insert values
 11        (x.employee_id,x.first_name
 12        ,x.last_name,x.email
 13        ,x.phone_number,x.hire_date
 14        ,x.job_id,x.salary
 15        ,x.commission_pct,x.manager_id
 16        ,x.department_id  )
 17    returning employee_id, last_name
 18    bulk collect into l_enums, l_names;
 19
 20    for i in l_enums.first .. l_enums.last
 21    loop
 22      dbms_output.put_line('emp-'||l_enums(i)||':'||l_names(i));
 23    end loop;
 24  end;
 25  /
declare
*
ERROR at line 1:
ORA-00932: expression is of data type -, which is incompatible with expected data type NUMBER
ORA-06512: at line 5

I played around for a while, and it turned out that the “1=0” in the ON condition was the cause of PLSQL’s complaint. Once I changed that to the more conventional column join, but still ensuring it was always false, the code now works.


SQL> declare
  2    l_enums  sys.odcinumberlist;
  3    l_names  sys.odcivarchar2list;
  4  begin
  5    merge into emp_temp t
  6    using ( select * from hr.employees
  7            where department_id = 30 ) x
  8    on (x.employee_id = x.employee_id-1)
  9    when not matched then
 10      insert values
 11        (x.employee_id,x.first_name
 12        ,x.last_name,x.email
 13        ,x.phone_number,x.hire_date
 14        ,x.job_id,x.salary
 15        ,x.commission_pct,x.manager_id
 16        ,x.department_id  )
 17    returning employee_id, last_name
 18    bulk collect into l_enums, l_names;
 19
 20    for i in l_enums.first .. l_enums.last
 21    loop
 22      dbms_output.put_line('emp-'||l_enums(i)||':'||l_names(i));
 23    end loop;
 24  end;
 25  /
emp-114:Li
emp-115:Khoo
emp-116:Baida
emp-117:Tobias
emp-118:Himuro
emp-119:Colmenares

PL/SQL procedure successfully completed.

SQL>

And there we go. You can have INSERT-SELECT-RETURNING by using a MERGE command!

But…watch this space. We might have some even more powerful SQL extensions coming your way in future release updates which make things even easier.

Got some thoughts? Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Trending