Skip to Main Content
  • Questions
  • Insert All with sequence - feature or featurette?

Breadcrumb

Question and Answer

Connor McDonald

Thanks for the question, John.

Asked: November 29, 2016 - 11:55 am UTC

Last updated: March 22, 2018 - 2:18 am UTC

Version: 11.2.0.4

Viewed 10K+ times! This question is

You Asked

I posted this problem elsewhere and we had a bit of a discussion. However I would like to know whether what we see is a feature (i.e. it can be relied upon to be somewhat consistent in future releases) or whether it is a "featurette" - i.e. unintended functionality that could well be "fixed" in future releases.

If I set up the following:

Create Sequence xxseq;
Create Table xxa (a Number);
Create Table xxb (a Number);

Insert All
  When rn = 1 Then Into xxa(a) Values (xxseq.nextval)
  When 1=1 Then Into xxb(a) Values (xxseq.currval)
Select Level rn From Dual Connect By Level <= 5;

Select a From xxa;
Select a From xxb;


Then I see the following results:

         A
----------
         2
1 row selected.

         A
----------
         2
         3
         4
         5
         6

5 rows selected.


I.e. the sequence appears to get incremented for all rows, regardless of whether that row matches the first When clause. I presume this is because the values in each when clause are just pushed down into the database as a single select statement.

However - when I subsequently do this:

Create Function xxgetseq(p_method In Varchar2) Return Number
Is
Begin
  If p_method = 'NEXTVAL' Then
    Return xxseq.nextval;
  Elsif p_method = 'CURRVAL' Then 
    Return xxseq.currval;
  End If;
End;
/

Delete xxa;
Delete xxb;
Drop Sequence xxseq;
Create Sequence xxseq;

Insert All
  When rn = 1 Then Into xxa(a) Values (xxgetseq(p_method=>'NEXTVAL'))
  When 1=1 Then Into xxb(a) Values (xxgetseq(p_method=>'CURRVAL'))
Select Level rn From Dual Connect By Level <= 5;

Select a From xxa;
Select a From xxb;


I see that the sequence is only incremented on the matching row, and not on subsequent rows.
         A
----------
         1
1 row selected.

         A
----------
         1
         1
         1
         1
         1

5 rows selected.


Why is that? Why does wrapping the sequence in a function cause the code not to be executed? Is this a feature or not?

Thanks

with LiveSQL Test Case:

and Chris said...

This belongs in the "featurette" category for me. As the docs say:

You cannot specify a sequence in any part of a multitable insert statement. A multitable insert is considered a single SQL statement. Therefore, the first reference to NEXTVAL generates the next number, and all subsequent references in the statement return the same number.

http://docs.oracle.com/database/121/SQLRF/statements_9014.htm#i2095116

So using sequences in insert all is generally "not safe". As the following example shows, Oracle increments the value even though it only adds one row:

Create Sequence xxseq;
Create Table xxa (a Number);
Create Table xxb (a Number);

Insert All
  When rn = 1 Then Into xxa(a) Values (xxseq.nextval)
Select Level rn From Dual Connect By Level <= 5;

select * from xxa;

A  
1  

select xxseq.currval from dual;

CURRVAL  
5   

Rating

  (6 ratings)

Is this answer out of date? If it is, please let us know via a Comment

Comments

Using a function instead

John Keymer, November 29, 2016 - 2:28 pm UTC

Hi,
Thanks for the feedback. I am told that by using a function to return the sequence instead of directly accessing the sequence exploits PL/SQL's optimization process. However would you consider that "unsafe"?

The reason I am asking is that I was trying to resolve an issue with efficiently creating a copy of master/detail record.


So setting up just some basic data:

Create Sequence xxmaster_s;  
Create Table xxmaster  
As  
      Select xxmaster_s.nextval id, 'Dummy' || To_char (Level, 'fm00') val  
        From Dual  
  Connect By Level <= 10;  
  
Create Table xxdetail  
As  
 Select Mod(Level,10) + 1 master_id, 'Dummy Detail' || To_char (Round(Dbms_Random.Value(1,Level)), 'fm00') val_dtl  
 From Dual Connect By Level <= 100;  


I now want to copy those master records 1-10 and their corresponding detail records, but assign them new sequences. So a "loops" way of doing this would be:

 
Declare  
  l_new_id xxmaster.id%Type;  
Begin  
  For i In (Select id, val From xxmaster) Loop  
    Insert into xxmaster(id, val) Values (xxmaster_s.nextval,i.val)  
    Returning id Into l_new_id;  
      
    Insert Into xxdetail(master_id, val_dtl)  
    Select l_new_id, d.val_dtl  
      From xxdetail d  
     Where d.master_id = i.id;  
  End Loop;  
End;  


But I find it somewhat inefficient - especially if we are considering large volumes of data in xxmaster because we're looping through and inserting one record at a time, then executing the insert of detail records for each.
Chris Saxon
November 29, 2016 - 5:41 pm UTC

Yes, the looped method will be inefficient!

A better way would be something like:

- Add a column to the master orig_id
- Insert the rows to copy, loading the original id into the new column
- You can then find the detail rows to load by joining this table to the orig_id column on the master table

Don't trust OTN?

Stew Ashton, November 29, 2016 - 9:31 pm UTC

I am not criticising at all, but I find it interesting that the OP asked this question in the OTN forum, then didn't fully trust the answer and came here for a second opinion.

The OTN thread is here: https://community.oracle.com/thread/3994872

I would not trust the OTN solution for two reasons:
- first, SQL not being a procedural language Oracle is free to call functions if and when it chooses, so there is no clear guarantee the behavior will not change.
- second, if there is a foreign key constraint from the detail rows to the master rows then I have seen the constraint fail from time to time when doing INSERT ALL.

Chris' solution avoids both potential obstacles.

Another solution would be a pipelined table function that read the master and detail rows but used the sequence in a procedural manner to get new master ids. This would work if the data were being copied to another table, but fails with a "mutating table" error when adding new rows to the original table.

In most cases, where the source data is not in the target table, I would be tempted by the pipelined table function since it does everything in one statement without requiring any additional columns. To avoid the error on the foreign key constraint, I would sort the output of the pipelined table function to insert all the master rows first.

Best regards, Stew
Connor McDonald
November 30, 2016 - 1:52 am UTC

thanks for stopping by Stew

John Keymer, November 30, 2016 - 8:29 am UTC

Hi,
The reason for coming here was more to get clarification on the function optimization and whether that could be relied upon. As stated though, it could (in theory) change at any time.

The solution Chris proposed does seem to be the simplest, and if anything provides a useful audit trail for where the records were copied from.

Thanks
Chris Saxon
November 30, 2016 - 10:38 am UTC

A bigger concern for me is insert all doesn't guarantee order of execution of inserts. So you could find the child inserted before the parent, giving you the "old" currval!

Would the following be helpful.

GJ, November 30, 2016 - 10:22 am UTC

Step 1.
Insert into the master select new_SEQ, OTHER_cols from master table

Step 2.
Insert into detail
select a.new_id,b.val_dtl
from(select max(id) as new_id,
,min(id) as old_id
,val
from master
group by val
)a
join detail b
on a.old_id=b.master_id


Chris Saxon
November 30, 2016 - 10:40 am UTC

That's only going to work if val is unique!

RE

GJ, November 30, 2016 - 3:44 pm UTC

If its a master table i expect there would be some columns which form a natural key.

If val is not unique, there could be a set of columns which are unique in the master table that would form the natural key of the master table.

The OP can confirm if this is the case.
Chris Saxon
November 30, 2016 - 3:59 pm UTC

Yes, there should be some form of natural key. But as you have to drop any unique constraints on this to add the copies you can't be certain there won't be any duplicates...

Is this expected functionality?

John Keymer, March 21, 2018 - 1:41 pm UTC

Hi,
I was trying to achieve something else today which is similar to this but slightly different - inserting a number of very similar rows into a single table with an identity column defined. Obviously I could have done this with multiple insert statements, however my first attempt was with INSERT ALL - which failed with a primary key error.

I set up a very simple example here: https://livesql.oracle.com/apex/livesql/s/gfq045vbmgdnsapiwzns0guoz

Connected to:
Oracle Database 12c Enterprise Edition Release 12.2.0.1.0 - 64bit Production

SQL> set echo on
SQL> @a
SQL> create table a(b number generated always as identity, c number, constraint a_pk primary key (b));

Table created.

SQL>
SQL>
SQL> insert all into a(c) values (lvl)
  2             into a(c) values (lvl*10)
  3  select level lvl from dual connect by level <= 2;
insert all into a(c) values (lvl)
*
ERROR at line 1:
ORA-00001: unique constraint (KEYMEJ.A_PK) violated


SQL>
SQL> drop table a;

Table dropped.

SQL>
SQL> -- Do it the long way
SQL>
SQL> create table a(b number, c number, constraint a_pk primary key (b));

Table created.

SQL>
SQL> create sequence a_s;

Sequence created.

SQL>
SQL> create trigger a_pk_trg before insert on a for each row
  2  begin
  3    :new.b := a_s.nextval;
  4  end;
  5  /

Trigger created.

SQL>
SQL> insert all into a(c) values (lvl)
  2             into a(c) values (lvl*10)
  3  select level lvl from dual connect by level <= 2;

4 rows created.

SQL>
SQL> select * from a;

         B          C
---------- ----------
         1          1
         3          2
         2         10
         4         20

SQL>
SQL> drop table a;

Table dropped.

SQL> drop sequence a_s;

Sequence dropped.

SQL>



Now I know there are a number of workarounds that can be used, however I was surprised to find that this failed as it did - I was expecting the identity column to be populated in a similar fashion to how a trigger works (i.e. for each row inserted into the database). Clearly that isn't the case though.

Is this intended/expected functionality??

Thanks
Connor McDonald
March 22, 2018 - 2:18 am UTC

It is not really related to identity columns, it is the way that sequences have always worked with multi-table insert.

See my comments here

https://jeffkemponoracle.com/2016/10/restriction-when-column-default-is-sequence-nextval/