Search code examples
sqloraclesql-insertanalytic-functions

auto increment logic in DML


I have two tables test2 and test_hist. i want to load data into test_hist from test2 but it is failing due to unique constraint .

CREATE TABLE TEST2 (ID NUMBER , TEXT VARCHAR2(10));
create table test_hist (id number , text varchar2(10) , constraint t_pk PRIMARY key (id , text));

INSERT INTO TEST2 VALUES(100 , '20180909-I');
INSERT INTO TEST2 VALUES(101 , '20180909-I');
INSERT INTO TEST2 VALUES(102 , '20180809-I');
INSERT INTO TEST2 VALUES(100 , '20180909-I');
COMMIT;

INSERT INTO test_hist SELECT ID , TEXT FROM TEST2;

I want to append the TEXT field with auto increment number whenever there is a duplicate like below.

expected OUTPUT 

ID         TEXT
100      20180909-I
101      20180909-I
102      20180809-I
100      20180909-I-1
100      20180909-I-2
102      20180809-I-1

Could any one help me to achieve this. Thanks in Advance

if i execute the insert statements multiple times it should inserted into the test_hist with autoincrement text . e.g

insert into test_hist
select id,
  text || case when row_number() over (partition by id, text order by null) > 1
          then (1 - row_number() over (partition by id, text order by null)) end
from test2;

9 rows inserted.

select *
from test_hist
order by id, text;

        ID TEXT        
---------- ------------
       100 20180909-I  
       100 20180909-I-1
       100 20180909-I-2
       100 20180909-I-3
       101 20180909-I  
       101 20180909-I-1
       102 20180809-I  
       102 20180809-I-1
       102 20180809-I-2 

Solution

  • Same basic idea as @Barbaros, but arranged slightly differently, and with the second table's column size increased so it can hold the amended value:

    create table test2 (
      id number, text varchar2(10)
    );
    
    create table test_hist (id number,
      text varchar2(12), -- increased size to 12; may need to be larger
      constraint t_pk primary key (id , text)
    );
    
    insert into test2 values(100 , '20180909-I');
    insert into test2 values(101 , '20180909-I');
    insert into test2 values(102 , '20180809-I');
    insert into test2 values(100 , '20180909-I');
    insert into test2 values(100 , '20180909-I');
    insert into test2 values(102 , '20180809-I');
    

    Then the same analytic function, in one level if you don't mind repeating it, and including all the PK columns in the partition-by clause:

    insert into test_hist
    select id,
      text || case when row_number() over (partition by id, text order by null) > 1
              then (1 - row_number() over (partition by id, text order by null)) end
    from test2;
    
    6 rows inserted.
    
    select *
    from test_hist
    order by id, text;
    
            ID TEXT        
    ---------- ------------
           100 20180909-I  
           100 20180909-I-1
           100 20180909-I-2
           101 20180909-I  
           102 20180809-I  
           102 20180809-I-1
    

    If you actually have more columns in your real scenario and there is another non-PK column in the original table which you want to influence the order the history rows are incremented, you can just use that in the function's order by instead of null, which is just a dummy placeholder really.


    If it needs to keep doing the same thing on multiple inserts (which suggests a data model issue even more than the original requirement did) then you'll need to count existing matches in the history table too.

    Starting from the same original data as before and an empty historytable:

    insert into test_hist
    select id,
      text || case when appearance > 1 then (1 - appearance) end
    from (
      select t.id,
        t.text,
        row_number() over (partition by t.id, t.text order by null) + (
          select count(*) from test_hist th
          where th.id = t.id
          and th.text like t.text || '%'
        ) as appearance
      from test2 t
    );
    
    6 rows inserted.
    
    select *
    from test_hist
    order by id, text;
    
            ID TEXT        
    ---------- ------------
           100 20180909-I  
           100 20180909-I-1
           100 20180909-I-2
           101 20180909-I  
           102 20180809-I  
           102 20180809-I-1
    
    6 rows selected. 
    

    and running the same statement a second time:

    insert into test_hist
    select id,
      text || case when appearance > 1 then (1 - appearance) end
    from (
      select t.id,
        t.text,
        row_number() over (partition by t.id, t.text order by null) + (
          select count(*) from test_hist th
          where th.id = t.id
          and th.text like t.text || '%'
        ) as appearance
      from test2 t
    );
    
    6 rows inserted.
    
    select *
    from test_hist
    order by id, text;
    
            ID TEXT        
    ---------- ------------
           100 20180909-I  
           100 20180909-I-1
           100 20180909-I-2
           100 20180909-I-3
           100 20180909-I-4
           100 20180909-I-5
           101 20180909-I  
           101 20180909-I-1
           102 20180809-I  
           102 20180809-I-1
           102 20180809-I-2
           102 20180809-I-3
    
    12 rows selected. 
    

    There are probably ways to optimise it so you don't have to hit the history table so often, but this may give you a starting point to work from.

    The use of like really means this only works if the text is always the same length, or at least you can't have values that are extensions of other values; otherwise you'll get more matches than you want. From your sample data, at least, that doesn't look like an issue. You could probably work around that by switching to regexp_like if necessary, depending on your actual data.