Creating BETTER auto-increment columns in Oracle

I was explaining to a developer today how Oracle doesn’t support auto-increment columns for IDs and how you have to implement your own. I wanted to point him to a tutorial but I found that most of the tutorials have PL/SQL code with unexplained side effects. So I thought I’d post some Oracle PL/SQL code that behaves just like a MySQL or PostgreSQL auto-increment column, but which also fixes the sequence number when an application provides its own ID number for a record, which is one place where MySQL and PostgreSQL autoincrement columns fail.

Step 1: Know the names of your tablespaces. Know which tablespace you want to use for your tables and which tablespace you want to use for your indexes. Don’t assume that Oracle is going to put the table in the right place. To get a list of available tablespaces:

select tablespace_name from user_tablespaces;

Step 2: Create your table. In this example the table is built in the tablespace TDATA and the primary key index is built in the tablespace INDX.

create table example (
    id number not null,
    name varchar2(30),
    constraint example_pk primary key(id) using index tablespace INDX
) tablespace TDATA;

Note that by declaring the id column “not null” you can’t add a record with an empty ID field, ever.

Creating the primary key constraint on a separate line allows us to name the constraint, which can be handy when you’re looking through the data dictionary later on trying to figure out which constraint does what.

Step 3: Create a sequence. In this case I create an ID sequence that starts at 1 and goes on from there. If you want to start your ID sequence with a different number, or increment by some other amount, change these values.

create sequence example_id_seq start with 1 increment by 1;

Step 4: Create a trigger. This is what does the actual work setting the ID number. There are many ways to do this, here are two:

Method 1: Use a sequence-generated ID for all IDs.

create or replace trigger example_insert
before insert on example
for each row
begin
    select example_id_seq.nextval into :new.id from dual;
end;
/

Method 1 will always use a sequence-generated ID no matter what. For example:

SQL> insert into example (id, name) values (100, 'Earl');
1 row created.
SQL> insert into example (name) values ('Cleopatra');
1 row created.
SQL> insert into example (name) values ('Frankenstein');
1 row created.
SQL> select * from example;
        ID NAME
---------- ------------------------------
         1 Earl
         2 Cleopatra
         3 Frankenstein

In this case the trigger-supplied ID 1 overwrote the user-supplied id 100.

Method 2: Allow users to supply their own IDs when they want to.

create or replace trigger example_insert
before insert on example
for each row
declare
    max_id number;
    cur_seq number;
begin
    if :new.id is null then
        -- No ID passed, get one from the sequence
        select example_id_seq.nextval into :new.id from dual;
    else
        -- ID was set via insert, so update the sequence
        select greatest(nvl(max(id),0), :new.id) into max_id from example;
        select example_id_seq.nextval into cur_seq from dual;
        while cur_seq < max_id
        loop
            select example_id_seq.nextval into cur_seq from dual;
        end loop;
    end if;
end;
/

Method 2 will use the sequence-generated ID if the insert statement doesn’t supply an ID, which is what MySQL and PostgreSQL do. It also updates the sequence so that the next value of the sequence won’t collide with IDs supplied by an insert statement.

For example, let’s say I’m using Trigger Method 2 and just loaded the table with a bunch of old data, including old ID numbers.

SQL> delete from example;
3 rows deleted.
SQL> insert into example (id, name) values (200, 'Cleopatra');
1 row created.
SQL> insert into example (id, name) values (300, 'Frankenstein');
1 row created.
SQL> insert into example (name) values ('Earl');
1 row created.
SQL> select * from example;

        ID NAME
---------- ------------------------------
       200 Cleopatra
       300 Frankenstein
       301 Earl

The last record added gets an ID of 301.

Hope you find this useful.

65 thoughts on “Creating BETTER auto-increment columns in Oracle

  1. dear sir/madam
    please tell me about data increment (autonumber) in oracel developer if the field is empty.
    when i press the new button it gave auto number.

    with many thanks

    Ghulam Yassen

    • Ghulam:

      What it sounds like you’re doing is creating a trigger that looks like the trigger in Method #1, which is the traditional way to make an Oracle field that behaves like an auto-number field. You can see the trigger you generated using the following SQL:

      select TRIGGER_NAME, TABLE_NAME, TRIGGER_BODY from user_triggers;

      — Earl

  2. Very good tutorial. I created auto-generated column in a table in few minutes following this tutorial.

    Thanks

  3. thanks earl!
    this works right up until i delete a row and then it doesnt pick up the increments(i.e. last insert before the delete was 5 so it should insert the next row as 6 but instead inserts it as 21). do u have any suggestions please!

  4. earl,
    thanks for the quick reply. im still online trying to trouble shoot this.
    i used your exact statements (copy and pasted), i even created your example table to avoid any confusion and implemented the statements on that table. i am using oracle 10g XE if that makes a difference..
    thanks!

    • I actually meant the insert/delete statements you used. I wasn’t clear on when you were setting the ID and when you were letting the trigger set the ID.

      If you ever set ID 20, the next record will have a 21. Deletions won’t affect this.

      More likely what you’re seeing a side effect of the sequence cache value. Since Oracle uses a default sequence cache value of 20, if you were on #5 and you had a server failure the then the next number in the sequence could be 21. See http://www.techonthenet.com/oracle/sequences.php. I’m not sure what else you can do to exhaust the cache (restarting Oracle doesn’t do it), which is why I was asking about the insert/delete statements you used.

      You might try recreating the sequence with the “nocache” option and see if you still see this behavior, although I wouldn’t recommend that if you’re working on a database with a large amount of transaction activity.

  5. This suggestion is quite useful if we are inserting records in a single table. However, what would happen if the auto incrementing column is a primary key in the header table and the same is being referenced in the detail table. Also we want to insert records in both header and detail tables in one transaction.

    • Rakesh: The triggers still work exactly the same in the case you’re describing. What you want to do is to use the “returning” clause on your insert statement to return the primary key column’s new value, use that as the foreign key in your detail table, then commit after all of the records have been inserted (in a single transaction). Make sure that autocommit has been turned off in your session.

      When I’m writing software I usually call a utility function to open the database connection and set up the initial session state so it’s always the same: autocommit off, NLS_DATE_FORMAT=’YYYY-MM-DD HH24:MI:SS’, etc. You can do the same thing with login.sql to ensure that your sqlplus sessions always start with a known state. If you do that and all parts of your application use the same “get database connection” utility function then your SQL statements will always execute with the environment in a known state.

  6. how would i do this if my table already exists ? i cant be adding the id column since it is marked non-nullable.

    • Andy:

      Add the ID column as nullable field.

      Update the ID column with a unique ID value. (update tablename set id = rownum where id is null;)

      Alter the table and make the ID column non-nullable.

      Alter the table and add the unique constraint.

      — Earl

  7. Thx, but it is not clear for me where you mean ID as the keyword or where id as the name of the column.

    e.g.
    “if :new.id ”

    Have I change this to new.group_id in the case my column is called grop_id?

    In my case the table hast the columns
    id, group_id, name
    id = autoincrement
    group_id takes another sequence

    Thx in advance!

    • :new refers to the record/row that triggered a “for each row” trigger.

      In the statement “select greatest(max(id), :new.id) into max_id from example;” the max(id) is the maximum value of the id field looking at the entire table, :new.id is the value of the id field of the row that triggered the trigger.

      The statement “if :new.id” refers to the value of the ID field of the row that triggered the trigger.

      You might want to review a PL/SQL tutorial and read up on :old and :new. There are nuances in their use that I’m not covering here.

  8. Thanks allot :-)

    as Oracle is new for me ( coming from MySQL) I have to say that I’m amazed that Oracle did not have such function!

    but Thanks allot :-) you just made my day ;-)

    cheers

  9. I have just one question?

    create or replace trigger example_insert
    before insert on example
    for each row
    begin
    select example_id_seq.nextval into :new.id from dual;
    end;

    what is new.id ???
    what is dual ???

    what do they present, and how do I modify them to my need (just out of curiosity)?

  10. Hi Earl, first of all thanks for your article…

    Just i have one question.
    Is really necesary to put “using index tablespace INDX” the tablespace when we are defining the index ?

  11. If you want to force Oracle to use a particular tablespace for your index then yes, it’s necessary.

    If you don’t care what tablespace Oracle sticks your index into then you can leave that part off.

  12. Why would PostgreSQL fail in this situation? You can create (almost) the same trigger and function in plpgsql to do exactly the same trick as you do in Oracle.

  13. eloo..
    i got problem..i run sequence code are created..but when i want to create trigger in TOAD he prompt me to insert value??
    what value should i put in dialog box..tq

  14. Frank: I was referring specifically to the typical MySQL auto_increment and Postgres “SET DEFAULT NEXTVAL(‘some_sequence’)” usage, which is also the typical way that you implement an auto-increment sequence in Oracle. In all three cases the sequence will fail to insert a new record if the ID already exists.

    You could definitely implement this as a trigger in Postgres.

  15. Hi Earl,

    Thnx for the article :)

    I hav a question. Why did you go for getting the next value for the sequence in the below statement when current value seems to be workable?

    “select example_id_seq.nextval into cur_seq from dual;”

  16. Thanks for the helpful scripts but… some results were not expected.

    I executed the below script by order

    create table example (
    id number not null,
    name varchar2(30),
    constraint example_pk primary key(id)
    );

    create sequence example_id_seq start with 1 increment by 1;

    create or replace trigger example_insert
    before insert on example
    for each row
    declare
    max_id number;
    cur_seq number;
    begin
    if :new.id is null then
    — No ID passed, get one from the sequence
    select example_id_seq.nextval into :new.id from dual;
    else
    — ID was set via insert, so update the sequence
    select greatest(max(id), :new.id) into max_id from example;
    select example_id_seq.nextval into cur_seq from dual;
    while cur_seq < max_id
    loop
    select example_id_seq.nextval into cur_seq from dual;
    end loop;
    end if;
    end;
    /

    select * from example;

    insert into example (id, name) values (100, 'Earl');
    insert into example (name) values ('Cleopatra');
    insert into example (name) values ('Frankenstein');

    commit;

    select * from example;

    Results:
    1) table created
    2) sequence created
    3) trigger created
    4) successfully executed but no records (expected)
    5) 1 record inserted
    6) 1 record inserted
    7) 1 record inserted
    4) successfully executed with 3 records (expected)
    but the id field's value are not

    ID,NAME
    100,Earl
    2,Cleopatra
    3,Frankenstein

    Possible to fail when the sequence reaches the 100.

    Please give a comment on this.

    my env:
    Oracle 11g

  17. additional, here’s the sequence details

    CREATED 10-11-19
    LAST_DDL_TIME 10-11-19
    SEQUENCE_OWNER xxx
    SEQUENCE_NAME EXAMPLE_ID_SEQ
    MIN_VALUE 1
    MAX_VALUE 999999999999999999999999999
    INCREMENT_BY 1
    CYCLE_FLAG N
    ORDER_FLAG N
    CACHE_SIZE 20
    LAST_NUMBER 21

  18. additional, please take a look with my experiment.

    continuation with the previous post, I tried to execute the following scripts in order:

    insert into example (id, name) values (1000, ‘Earl2’);

    insert into example (name) values (‘Cleopatra2’);

    select * from example;

    INSERT Results:
    1 record inserted
    1 record inserted

    Select Result:
    ID NAME
    ———————- ——————————
    100 Earl
    2 Cleopatra
    3 Frankenstein
    1000 Earl2
    1001 Cleopatra2

    Conclusion:
    it fails only on the very first time.

  19. and here’s the another test (expected):

    truncate table example;
    insert into example (id, name) values (100, ‘Earl’);
    insert into example (name) values (‘Cleopatra’);
    insert into example (name) values (‘Frankenstein’);
    commit;

    1 record inserted
    1 record inserted
    1 record inserted
    commit succeed

    ID NAME
    ———————- ——————————
    100 Earl
    1003 Cleopatra
    1004 Frankenstein

    note: correction on November 18, 2010 @ 10:17 pm post..
    before the SELECT statement, I successfully executed the commit.

  20. Yes, I confirmed. The cause is the greatest function failed to get compare when the target table has no record.

    try this:

    truncate table example;
    select greatest(max(id), 100) from example;

    Result2:
    truncate table example was successfully executed.
    GREATEST(MAX(ID),100)
    ———————-

    1 rows selected

    …but the expected value is 100.

    I think the behavior of greatest function is different if encountered NULL.

    here’s the link: http://asktom.oracle.com/pls/asktom/f?p=100:11:0::::P11_QUESTION_ID:524526200346472289

  21. Here’s the revised trigger script of Method 2.
    Add NVL for NULL checking. This is recommended only if the field is a number.

    create or replace trigger example_insert
    before insert on example
    for each row
    declare
    max_id number;
    cur_seq number;
    begin
    if :new.id is null then
    — No ID passed, get one from the sequence
    select example_id_seq.nextval into :new.id from dual;
    else
    — ID was set via insert, so update the sequence
    select greatest(NVL(max(id), 0), :new.id) into max_id from example;
    select example_id_seq.nextval into cur_seq from dual;
    while cur_seq < max_id
    loop
    select example_id_seq.nextval into cur_seq from dual;
    end loop;

    end if;
    end;
    /

  22. You are correct, on an empty table if the very first record inserted had an ID passed the max(id) would return null. nvl() is the correct way to fix that problem.

    Good catch. I’ll update the example code.

  23. Pingback: My Readings This Week – Ganda Manurung

  24. Auto increment of the column in 11G without any trigger only applicable in 11g

    CREATE SEQUENCE “CM_4_0″.”TEST_SEQ” MINVALUE 1 MAXVALUE 9999999999999999999999999999 INCREMENT BY 2 START WITH 1000031624 CACHE 20 NOORDER NOCYCLE ;

    create or replace function F return number as
    a number;
    begin return
    select TEST_SEQ.NEXTVAL into a from DUAL;
    return a;
    end;

    drop table Z

    create table Z( name varchar2(10), PRI number generated always as(f()) VIRTUAL );

    insert into z(name)values(‘asasa’);
    insert into Z(name)values(‘asasa’);
    insert into z(name)values(‘asasa’);
    insert into z(name)values(‘asasa’);

    commit;

  25. Michele:

    If you’re worried about performance test it and see. Try inserting a million records into a database using the method1 trigger, then try it again with the method2 trigger. Time the results.

    Let us know what you find.

  26. Pingback: Creating auto-incremented PK column in Oracle as we know it from SQL Server « Jacek Spólnik's blog

  27. I was having the same problem as mona with Oracle 10g XE. However I’m not sure it is a XE-specific issue. I suspect it is related to the JDBC driver not handling the semicolons properly; it looks like it is trying to process it as multiple SQL statements. My guess is that mona was trying to run in a JDBC-based tool like DB Visualizer or SQL Developer. There may be a way to properly escape the semicolons but I gave up looking for it and found an easier way: try executing from SQL Plus, it worked for me.

  28. im so grateful for this post i had this problem for 3 good days. when the autonumber column is a string then its a problem if the table has no data but it works well when its a number.

  29. there is better option then creatin a trigger

    SQL> insert into example (id, name) values (example_id_seq.nextval, ‘Earl111’);
    1 row created.

    here we are not suppore to use trigger. nextval will take care fo the same.

  30. Method 2 also has an interesting issue if a user decides to put in all 9’s for a number, either to have something show at the bottom of a list or because they want something easy to remember. Would be better to see if the new id from the sequence is already in the database [as in your example, where the seq.nextval would be 300 and 300 has already been entered for a previous record by the user]and find the next unused value [loop through the .nextval until you find one that hasn’t been used].

  31. I tried using method one with some modifications and got some problems. I use oracle 11g and sql developer to learn.
    When i tried to create the trigger, i got a diaglog box asking me to “enter bind values”. I just did what the box said without looking too much into it.
    The trigger was created with a warning. I don’t know if this is dangerous.
    The details are posted to stack overflow –

    http://stackoverflow.com/questions/12166046/creating-a-trigger-in-oracle-express/12167586#12167586

    http://stackoverflow.com/questions/12167191/when-i-create-a-trigger-i-get-warning-execution-completed-with-warning

    Please help me to fix this.
    Thanks in advance.

  32. :new is a type of bind variable, but in the context of the trigger it represents the newly-created record. I don’t use SQL Developer. Sounds like it wants to interpret :new as an interactive bind variable, which is not what it is.

    Try creating the trigger in an SQLPLUS session.

    Put your code into a text file such as “better_increment.sql”, start sqlplus, then type “@better_increment.sql” to run the code in the file. If you made any typos and get an error type “show errors” to find the mistake.

  33. can u help me how to use varchar auto increment in procedures like using auto increment numbers ex: select nvl(max(id,100)+1) into n from dual;
    i want to start the auto increment value like T0001, how can i do this, plz help me………..

  34. Dear Sir ,
    i want to know that , how i can create a auto increment id field which will set to 1 after deleting all the rows from the table .. please help me out … thanks

  35. I’d appreciate you help me to solve a problem. I have imported a database to my laptop, but it fail when I insert a row in the tables that has a auto increment index. Before, when I did the import the process failed because Oracle don’t found a tablespace. I created to hand this tablespace and I get did the import process successful. You may to help me with my problem?

  36. While it is easy to understand it’s truly awful in terms of efficiency. With method 2 if I add a couple of nulls and then add 10000000 it will loop about 10 million times until the sequence has moved on – not very nice!

    It could be worse. If the user adds negative values luckily the sequence is considered. You hate to have a “wait until the sequence reaches -1, incrementing upwards”.

  37. Why do so many other flavors of SQL have simple, single keywords like AUTOINCREMENT or AUTO_INCREMENT that instantly do what that huge mess of code that Oracle needs? A need to auto-increment is *VERY* common. I have it in EVERY table I have ever created in my lifetime.

    Will Oracle ever moving into the 20th century? (Let alone the 21st century.)

  38. I want new record automatically increment id form builders code;
    Please help me.

  39. hey i have this error when try to insert new row to my table after doing everything with method 1, the error is ORA-04098 trigger is invalid and failed re-validation.
    and my trigger is,
    create or replace trigger CCMSDBA.Scheduled_Id_insert
    before insert on ccmsdba.schedule_activity
    for each row
    begin
    select ccmsdba.scheduled_id_seq.nextval into :new.scheduled_id from dual;
    end;
    /

    • T: You should also get that error when you create the trigger. Try creating the trigger, then type “show errors” to find out what Oracle doesn’t like. You should get the line number that Oracle is choking on and a more detailed error message.

Leave a Reply

Your email address will not be published. Required fields are marked *

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