Best way to generate unique and consecutive numbers in Oracle - sql

I need to generate unique and consecutive numbers (for use on an invoice), in a fast and reliable way. I currently use an Oracle sequence, but in some cases, the generated numbers are not consecutive because of exceptions that may occur.
I thought a couple of solutions to manage this problem, but neither of them convincing me. What solution do you recommend?
Use a select max ()
SELECT MAX (NVL (doc_num, 0)) +1 FROM invoices
Use a table to store the last number generated for the invoice.
UPDATE docs_numbers
SET last_invoice = last_invoice + 1
Another Solution?

Option 1 can always be made to fail some way in an environment with concurrent
users.
Option 2 will work, but will limit scalability — obligatory Tom Kyte
reference:
http://asktom.oracle.com/pls/asktom/f?p=100:11:0::::P11_QUESTION_ID:1508205334476
As he recommends, you should really review the necessity for the "no gaps" requirement.

The gaps appear if a transaction uses a sequence number but is then rolled back.
Maybe the answer is not to assign the invoice number until the invoice can't be rolled back. This minimizes (but probably does not eliminate) the possibilities of gaps.
I'm not sure that there is any swift or easy way to ensure no gaps in the sequence - scanning for MAX, adding one, and inserting that is probably the closest to secure, but is not recommended for performance reasons (and difficulties with concurrency) and the technique won't detect if the latest invoice number is assigned, then deleted and reassigned.
Can you account for gaps somehow - by identifying which invoice numbers were 'used' but 'not made permanent' somehow? Could an autonomous transaction help in doing that?
Another possibility - assuming that gaps are relatively few and far between.
Create a table that records sequence numbers that must be reused before a new sequence value is grabbed. Normally, it would be empty, but some process that runs every ... minute, hour, day ... checks for gaps and inserts the missed values into this table. All processes first check the table of missed values, and if there are any present, use a value from there, going through the slow process of updating the table and removing the row that they use. If the table is empty, then grab the next sequence number.
Not very pleasant, but the decoupling of 'issuing invoice numbers' from 'scan for missed values' means that even if the invoicing process fails for some thread when it is using one of the missed values, that value will be rediscovered to be missing and re-reissued next time around - repeating until some process succeeds with it.

Keep the current sequence - you can use the following to reset the value to the maximum of what is currently stored in the table(s):
-- --------------------------------
-- Purpose..: Resets the sequences
-- --------------------------------
DECLARE
-- record of temp data table
TYPE data_rec_type IS RECORD(
sequence_name VARCHAR2(30),
table_name VARCHAR2(30),
column_name VARCHAR2(30));
-- temp data table
TYPE data_table_type IS TABLE OF data_rec_type INDEX BY BINARY_INTEGER;
v_data_table data_table_type;
v_index NUMBER;
v_tmp_id NUMBER;
-- add row to temp table for later processing
--
PROCEDURE map_seq_to_col(in_sequence_name VARCHAR2,
in_table_name VARCHAR2,
in_column_name VARCHAR2) IS
v_i_index NUMBER;
BEGIN
v_i_index := v_data_table.COUNT + 1;
v_data_table(v_i_index).sequence_name := in_sequence_name;
v_data_table(v_i_index).table_name := in_table_name;
v_data_table(v_i_index).column_name := in_column_name;
END;
/**************************************************************************
Resets a sequence to a given value
***************************************************************************/
PROCEDURE reset_seq(in_seq_name VARCHAR2, in_new_value NUMBER) IS
v_sql VARCHAR2(2000);
v_seq_name VARCHAR2(30) := in_seq_name;
v_reset_val NUMBER(10);
v_old_val NUMBER(10);
v_new_value NUMBER(10);
BEGIN
-- get current sequence value
v_sql := 'SELECT ' || v_seq_name || '.nextval FROM DUAL';
EXECUTE IMMEDIATE v_sql
INTO v_old_val;
-- handle empty value
v_new_value := in_new_value;
if v_new_value IS NULL then
v_new_value := 0;
END IF;
IF v_old_val <> v_new_value then
IF v_old_val > v_new_value then
-- roll backwards
v_reset_val := (v_old_val - v_new_value) * -1;
elsif v_old_val < v_new_value then
v_reset_val := (v_new_value - v_old_val);
end if;
-- make the sequence rollback to 0 on the next call
v_sql := 'alter sequence ' || v_seq_name || ' increment by ' ||
v_reset_val || ' minvalue 0';
EXECUTE IMMEDIATE (v_sql);
-- select from the sequence to make it roll back
v_sql := 'SELECT ' || v_seq_name || '.nextval FROM DUAL';
EXECUTE IMMEDIATE v_sql
INTO v_reset_val;
-- make it increment correctly again
v_sql := 'alter sequence ' || v_seq_name || ' increment by 1';
EXECUTE IMMEDIATE (v_sql);
-- select from it again to prove it reset correctly.
v_sql := 'SELECT ' || v_seq_name || '.currval FROM DUAL';
EXECUTE IMMEDIATE v_sql
INTO v_reset_val;
END IF;
DBMS_OUTPUT.PUT_LINE(v_seq_name || ': ' || v_old_val || ' to ' ||
v_new_value);
END;
/*********************************************************************************************
Retrieves a max value for a table and then calls RESET_SEQ.
*********************************************************************************************/
PROCEDURE reset_seq_to_table(in_sequence_name VARCHAR2,
in_table_name VARCHAR2,
in_column_name VARCHAR2) IS
v_sql_body VARCHAR2(2000);
v_max_value NUMBER;
BEGIN
-- get max value in the table
v_sql_body := 'SELECT MAX(' || in_column_name || '+0) FROM ' ||
in_table_name;
EXECUTE IMMEDIATE (v_sql_body)
INTO v_max_value;
if v_max_value is null then
-- handle empty tables
v_max_value := 0;
end if;
-- use max value to reset the sequence
RESET_SEQ(in_sequence_name, v_max_value);
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('Failed to reset ' || in_sequence_name ||
' from ' || in_table_name || '.' ||
in_column_name || ' - ' || sqlerrm);
END;
BEGIN
--DBMS_OUTPUT.ENABLE(1000000);
-- load sequence/table/column associations
/***** START SCHEMA CUSTOMIZATION *****/
map_seq_to_col('Your_SEQ',
'your_table',
'the_invoice_number_column');
/***** END SCHEMA CUSTOMIZATION *****/
-- iterate all sequences that require a reset
FOR v_index IN v_data_table.FIRST .. v_data_table.LAST LOOP
BEGIN
RESET_SEQ_TO_TABLE(v_data_table(v_index).sequence_name,
v_data_table(v_index).table_name,
v_data_table(v_index).column_name);
END;
END LOOP;
END;
/
-- -------------------------------------------------------------------------------------
-- End of Script.
-- -------------------------------------------------------------------------------------
The example is an anonymous sproc - change it to be proper procedures in a package, and call it prior to inserting a new invoice to keep the numbering consistent.

I think you'll find that using the MAX() of the existing numbers is prone to a new and exciting problem - duplicates can occur if multiple invoices are being created at the same time. (Don't ask me how I know...).
A possible solution is to derive the primary key on your INVOICE table from a sequence, but have this NOT be the invoice number. After correctly and properly creating your invoice, and after the point at which an exception or user's whim could cause the creation of the invoice to be terminated, you go to a second sequence to get the sequential number which is presented as "the" invoice number. This means you'll have two unique, non-repeating numbers on your INVOICE table, and the obvious one (INVOICE_NO) will not be the primary key (but it can and should be UNIQUE) so there's a bit of evil creeping in, but the alternative - which is to create the INVOICE row with one value in the primary key, then change the primary key after the INVOICE is created - is just too evil for words. :-)
Share and enjoy.

If you really want to have no gaps, you need to completely serialize access, otherwise there will always be gaps. The reasons for gaps are:
rollback
shutdown abort

It's not clear what you mean by 'because of exceptions that may occur'. If you want number NOT to be incremented if your transaction eventually rolls back then SEQUENCE is not going to work for you, because as far as I know, once NEXTVAL is requested from sequence the sequence position is incremented and rollback won't reverse it.
If this is indeed a requirements then you probably would have to resort of storing current counter in a separate table, but beware of concurrent updates - from both 'lost update' and scalability prospective.

I've come across this problem before. In one case, we were able to convince the business to accept that "real" invoices might have gaps, and we wrote a job that ran every day to "fill in" the gaps with "void" invoices for audit purposes.
In practice, if we put NOCACHE on the sequence, the number of gaps would be relatively low, so the auditors will usually be happy as long as their query on the "void" invoices don't return too many results.

You might have to re-think your process slighty and break it into more steps. Have one non-transactional step create the placeholder invoice (this not being in the transaction should eliminate gaps) and then within the transaction do the rest of your business. I think that was how we did it in a system I was stuck with years ago but I can't remember - I just remember it was "weird."
I'd say the sequence will guarantee unique/consecutive numbers but when you throw transactions in the mix that can't be guaranteed unless the sequence generation isn't within that transaction.

dpbradley's link in #2 sounds like your best bet. Tom keeps the transactionality with the caller, if you don't want that you could make it an autonomous transaction like so:
create or replace
function getNextInvoiceNumber()
return number is
l_invoicenum number;
pragma autonomous_transaction;
begin
update docs_numbers
set last_invoice = last_invoice + 1
returning last_invoice
into l_invoicenum;
commit;
return l_invoicenum;
exception
when others then
rollback;
raise;
end;

What we do is issue a sequence number to the transaction and then when the item we are processing is finalized we issue a permanent number (also a sequence).
Works well for us.
Regards
K

Related

How to send cursor response from Oracle stored procedure

I have a stored procedure which I want to return cursor as the OUT parameter. I see the output in
dbms_output.put_line(as_return_val); like
Loan Purpose can not be Limited || /loanPurpose
THE PURPOSE OF PURCHASE IS NOT VALID || /loanPurpose
I need to read this response as a array of Objects(each ro one Object) in my service layer. Sorry I am not sure I need to send it as a cursor or like a varchar from here. I added as_return_val this variable to see whether I am getting correct response .
Procedure
create or replace PROCEDURE PR_LOGIC_CHECK_TEST
(in_loan_id IN NUMBER,
in_trans_id IN NUMBER)AS
BEGIN
DECLARE
as_errm VARCHAR2(2000);
curr_cursor_out SYS_REFCURSOR;
temp_cursor_row temp_cor_ll_cursor%rowtype;
as_return_val VARCHAR2(500);
BEGIN
pr_loan_logic_check(in_loan_id, in_trans_id, as_errm, curr_cursor_out);
LOOP
FETCH curr_cursor_out INTO temp_cursor_row;
EXIT WHEN curr_cursor_out%notfound;
as_return_val := temp_cursor_row.ret_value
|| ' || '
|| '/'||temp_cursor_row.xpath_name;
--dbms_output.put_line(as_return_val);
END LOOP;
close curr_cursor_out;
ROLLBACK;
END;
END PR_LOGIC_CHECK_TEST;
I'm still guessing at what you really want to happen since you haven't actually given us a reproducible test case (i.e. something that we can run locally or on dbfiddle or liveSQL that produces a particular output given a particular input).
Architecturally, it seems problematic to have a stored procedure whose only purpose is to format the output of a different procedure for human consumption. Formatting results would more properly be done in the display layer (the view of an MVC application but view means something else when we're talking about databases so I'm using "display layer") of the application not in a stored procedure. It would make more sense for your application to call pr_loan_logic_check directly and to let your display layer decide to concatenate the values from multiple columns together. If someone wants to change how the output is formatted later, you'd then just be changing code in the display layer not in your backend database.
My guess is that you want to return a collection like this. Note that I'm creating the collection type at the SQL level. You could create it in a PL/SQL package as well.
create or replace type varchar2_tbl
is table of varchar2(500);
create or replace PROCEDURE PR_LOGIC_CHECK_TEST (
in_loan_id IN NUMBER,
in_trans_id IN NUMBER,
out_strings OUT varchar2_tbl
)
AS
as_errm VARCHAR2(2000);
curr_cursor_out SYS_REFCURSOR;
temp_cursor_row temp_cor_ll_cursor%rowtype;
BEGIN
pr_loan_logic_check(in_loan_id, in_trans_id, as_errm, curr_cursor_out);
out_strings := out_strings();
LOOP
FETCH curr_cursor_out INTO temp_cursor_row;
EXIT WHEN curr_cursor_out%notfound;
out_strings.extend();
out_strings( out_strings.count ) :=
temp_cursor_row.ret_value
|| ' || '
|| '/'||temp_cursor_row.xpath_name;
END LOOP;
close curr_cursor_out;
-- I have trouble imagining why you'd put a `rollback` here
ROLLBACK;
END PR_LOGIC_CHECK_TEST;

Trigger that restricts by date

Im trying to create a trigger that restricts the amount a reader can read in a given month.
CREATE OR REPLACE trigger Readings_Limit
Before update or insert on reading
for each row declare
readingcount integer;
max_read integer := 5;
Begin
Select count(*) into readingcount
from (select *
from Reading
where to_char(DateRead, 'YYYY-MM') = to_char(DateRead, 'YYYY-MM'))
where employeeid = :new.employeeid;
if :old.employeeid = :new.employeeid then
return;
else
if readingcount >= max_read then
raise_application_error (-20000, 'An Employee can only read 5 per month');
end if;
end if;
end;
This restricts the reader to 5 max in total no matter the month, i can't seem to get it to be 5 max each month. any ideas greatly appreciated!
Try to rewrite your trigger like this:
CREATE OR REPLACE trigger Readings_Limit
Before update or insert on reading
for each row
declare
readingcount integer;
max_read integer := 5;
Begin
Select count(*) into readingcount
from Reading
where DateRead between trunc(sysdate,'MM') and last_day(sysdate)
and employeeid = :new.employeeid;
if :old.employeeid = :new.employeeid then
return;
else
if readingcount >= max_read then
raise_application_error (-20000, 'An Employee can only read 5 per month');
end if;
end if;
end;
You add actual month into yout select and you avoid unnecessary date conversion.
I don't understand the condition
if :old.employeeid = :new.employeeid then
Does it mean, the trigger should not fire on updates? In this case it is better to make trigger only for insert or use clause if inserting then...
In order to properly create this validation using a trigger a procedure should be created to obtain user-specified locks so the validation can be correctly serialized in a multi-user environment.
PROCEDURE request_lock
(p_lockname IN VARCHAR2
,p_lockmode IN INTEGER DEFAULT dbms_lock.x_mode
,p_timeout IN INTEGER DEFAULT 60
,p_release_on_commit IN BOOLEAN DEFAULT TRUE
,p_expiration_secs IN INTEGER DEFAULT 600)
IS
-- dbms_lock.allocate_unique issues implicit commit, so place in its own
-- transaction so it does not affect the caller
PRAGMA AUTONOMOUS_TRANSACTION;
l_lockhandle VARCHAR2(128);
l_return NUMBER;
BEGIN
dbms_lock.allocate_unique
(lockname => p_lockname
,lockhandle => p_lockhandle
,expiration_secs => p_expiration_secs);
l_return := dbms_lock.request
(lockhandle => l_lockhandle
,lockmode => p_lockmode
,timeout => p_timeout
,release_on_commit => p_release_on_commit);
IF (l_return not in (0,4)) THEN
raise_application_error(-20001, 'dbms_lock.request Return Value ' || l_return);
END IF;
-- Must COMMIT an autonomous transaction
COMMIT;
END request_lock;
To have the least impact on scalability the serialization should be done at the finest level, which for this constraint is per employeeid and month. Types may be used in order to create variables to store this information for each row before the constraint is checked after the statement has completed. These types can be either defined in the database or (from Oracle 12c) in package specifications.
CREATE OR REPLACE TYPE reading_rec
AS OBJECT
(employeeid NUMBER(10) -- Note should match the datatype of reading.employeeid
,dateread DATE);
CREATE OR REPLACE TYPE readings_tbl
AS TABLE OF reading_rec;
The procedure and types can then be used in a compound trigger (assuming at least Oracle 11, this will need to be split into individual triggers in earlier versions)
CREATE OR REPLACE TRIGGER too_many_readings
FOR INSERT OR UPDATE ON reading
COMPOUND TRIGGER
-- Table to hold identifiers of inserted/updated readings
g_readings readings_tbl;
BEFORE STATEMENT
IS
BEGIN
-- Reset the internal readings table
g_readings := readings_tbl();
END BEFORE STATEMENT;
AFTER EACH ROW
IS
BEGIN
-- Store the inserted/updated readings
IF ( INSERTING
OR :new.employeeid <> :old.employeeid
OR :new.dateread <> :old.dateread)
THEN
g_readings.EXTEND;
g_readings(g_readings.LAST) := reading_rec(:new.employeeid, :new.dateread);
END IF;
END AFTER EACH ROW;
AFTER STATEMENT
IS
CURSOR csr_readings
IS
SELECT DISTINCT
employeeid
,trunc(dateread,'MM') monthread
FROM TABLE(g_readings)
ORDER BY employeeid
,trunc(dateread,'MM');
CURSOR csr_constraint_violations
(p_employeeid reading.employeeid%TYPE
,p_monthread reading.dateread%TYPE)
IS
SELECT count(*) readings
FROM reading rdg
WHERE rdg.employeeid = p_employeeid
AND trunc(rdg.dateread, 'MM') = p_monthread
HAVING count(*) > 5;
r_constraint_violation csr_constraint_violations%ROWTYPE;
BEGIN
-- Check if for any inserted/updated readings there exists more than
-- 5 readings for the same employee in the same month. Serialise the
-- constraint for each employeeid so concurrent transactions do not
-- affect each other
FOR r_reading IN csr_readings LOOP
request_lock('TOO_MANY_READINGS_'
|| r_reading.employeeid
|| '_' || to_char(r_reading.monthread, 'YYYYMM'));
OPEN csr_constraint_violations(r_reading.employeeid, r_reading.monthread);
FETCH csr_constraint_violations INTO r_constraint_violation;
IF csr_constraint_violations%FOUND THEN
CLOSE csr_constraint_violations;
raise_application_error(-20001, 'Employee ' || r_reading.employeeid
|| ' now has ' || r_constraint_violation.readings
|| ' in ' || to_char(r_reading.monthread, 'FMMonth YYYY'));
ELSE
CLOSE csr_constraint_violations;
END IF;
END LOOP;
END AFTER STATEMENT;
END;
You need to set the month you are looking at, so if you are considering the current month, make the inner query read like this:
( select * from Reading
where to_char(DateRead,'YYYY-MM') = to_char(DateRead,'YYYY-MM')
and to_char(sysdate,'YYYY-MM') = to_char(DateRead,'YYYY-MM'))
that way it will always compare to the current month and should move as your date moves.

How do I fix auto increment in oracle after a data refresh

Every time my team does a data refresh to our UAT environment we have issues with the 'auto incremented' columns in oracle They hold onto the old value and therefore cause errors when a new insert happens. The only solution I have found is to use
select test_SEQ.nextval from test_table;
Until the next sequence is bigger then the max seq number in the table. I have over 200 tables to update, is there an easier why to do this?
Thanks
Erin
One better way to do this would be to drop the sequences and create new ones with the desired START WITH value. You could generate the DDL to do this dynamically.
Check the following sqlfiddle http://sqlfiddle.com/#!4/17345/1
It doesn't completely work due to limitations in sqlfiddle, but here's the function that makes it happen:
create or replace function
reset_sequence(p_sequence_name varchar,
p_table_name varchar,
p_column_name varchar)
return integer is
v_temp integer;
v_sql varchar(2000);
begin
v_sql := 'select nvl(max('||p_column_name||'),0)+1 col_name from '||p_table_name;
execute immediate v_sql INTO v_temp;
v_sql := 'drop sequence '||p_sequence_name;
execute immediate v_sql;
v_sql := 'create sequence '||p_sequence_name||' start with '||v_temp;
execute immediate v_sql;
return v_temp;
end;
Basically you call this function and pass a schema name, table name and column name and it will set the function to the correct value. You can put a begin/exception/end block to ignore errors when dropping the sequence, in case it doesn't exist, but all that is just icing. You could also have it detect the column that is the primary key if you wanted, but no real way in Oracle to detect the sequence name.
You could also make a procedure version of this, but I tend to prefer functions for whatever reason.

Looping on values, creating dynamic query and adding to result set

I have the following problem. I am an experienced Java programmer but am a bit of a n00b at SQL and PL/SQL.
I need to do the following.
1 Pass in a few arrays and some other variables into a procedure
2 Loop on the values in the arrays (they all have the same number of items) and dynamically create an SQL statement
3 Run this statement and add it to the result set (which is an OUT parameter of the procedure)
I already have experience of creating an SQL query on the fly, running it and adding the result to a result set (which is a REF CURSOR) but I'm not sure how I'd loop and add the results of each call to the query to the same result set. I'm not even sure if this is possible.
Here's what I have so far (code edited for simplicity). I know it's wrong because I'm just replacing the contents of the RESULT_SET with the most recent query result (and this is being confirmed in the Java which is calling this procedure).
Any and all help would be greatly appreciated.
TYPE REF_CURSOR IS REF CURSOR;
PROCEDURE GET_DATA_FASTER(in_seq_numbers IN seq_numbers_array, in_values IN text_array, in_items IN text_array, list IN VARCHAR2, RESULT_SET OUT REF_CURSOR) AS
query_str VARCHAR2(4000);
seq_number NUMBER;
the_value VARCHAR2(10);
the_item VARCHAR2(10);
BEGIN
FOR i IN 1..in_seq_numbers.COUNT
LOOP
seq_number := in_seq_numbers(i);
the_value := trim(in_values(i));
the_item := trim(in_items(i));
query_str := 'SELECT distinct '||seq_number||' as seq, value, item
FROM my_table ai';
query_str := query_str || '
WHERE ai.value = '''||the_value||''' AND ai.item = '''||the_item||'''
AND ai.param = ''BOOK''
AND ai.prod in (' || list || ');
OPEN RESULT_SET FOR query_str;
END LOOP;
EXCEPTION WHEN OTHERS THEN
RAISE;
END GET_DATA_FASTER;
A pipelined table function seems a better fit for what you want, especially if all you're doing is retrieving data. See http://www.oracle-base.com/articles/misc/pipelined-table-functions.php
What you do is create a type for your output row. So in your case you would create an object such as
CREATE TYPE get_data_faster_row AS OBJECT(
seq NUMBER(15,2),
value VARCHAR2(10),
item VARCHAR2(10)
);
Then create a table type which is a table made up of your row type above
CREATE TYPE get_data_faster_data IS TABLE OF get_data_faster_row;
Then create your table function that returns the data in a pipelined manner. Pipelined in Oracle is a bit like a yield return in .net (not sure if you're familiar with that). You find all of the rows that you want and "pipe" them out one at a time in a loop. When your function completes the table that's returned consists of all the rows you piped out.
CREATE FUNCTION Get_Data_Faster(params) RETURN get_data_faster_data PIPELINED AS
BEGIN
-- Iterate through your parameters
--Iterate through the results of the select using
-- the current parameters. You'll probably need a
-- cursor for this
PIPE ROW(get_data_faster_row(seq, value, item));
LOOP;
LOOP;
END;
EDIT: Following Alex's comment below, you need something like this. I haven't been able to test this but it should get you started:
CREATE FUNCTION Get_Data_Faster(in_seq_numbers IN seq_numbers_array, in_values IN text_array, in_items IN text_array, list IN VARCHAR2) RETURN get_data_faster_data PIPELINED AS
TYPE r_cursor IS REF CURSOR;
query_results r_cursor;
results_out get_data_faster_row := get_data_faster_row(NULL, NULL, NULL);
query_str VARCHAR2(4000);
seq_number NUMBER;
the_value VARCHAR2(10);
the_item VARCHAR2(10);
BEGIN
FOR i IN 1..in_seq_number.COUNT
LOOP
seq_number := in_seq_numbers(i);
the_value := trim(in_values(i));
the_item := trim(in_items(i));
query_str := 'SELECT distinct '||seq_number||' as seq, value, item
FROM my_table ai';
query_str := query_str || '
WHERE ai.value = '''||the_value||''' AND ai.item = '''||the_item||'''
AND ai.param = ''BOOK''
AND ai.prod in (' || list || ');
OPEN query_results FOR query_str;
LOOP
FETCH query_results INTO
results_out.seq,
results_out.value,
results_out.item;
EXIT WHEN query_results%NOTFOUND;
PIPE ROW(results_out);
END LOOP;
CLOSE query_results;
END LOOP;
END;
Extra info from Alex's comment below useful for the answer:
you can have multiple loops from different sources, and as long as the
data from each be put into the same object type, you can just keep
pumping them out with pipe row statements anywhere in the function.
The caller sees them as a table with the rows in the order you pipe
them. Rather than call a procedure and get a result set as an output
parameter, you can query as select seq, value, item from
table(package.get_data_faster(a, b, c, d)), and of course you can
still have an order by clause if the order they're piped isn't what
you want.

Best way to reset an Oracle sequence to the next value in an existing column?

For some reason, people in the past have inserted data without using sequence.NEXTVAL. So when I go to use sequence.NEXTVAL in order to populate a table, I get a PK violation, since that number is already in use in the table.
How can I update the next value so that it is usable? Right now, I'm just inserting over and over until it's successful (INSERT INTO tbl (pk) VALUES (sequence.NEXTVAL)), and that syncs up the nextval.
You can temporarily increase the cache size and do one dummy select and then reset the cache size back to 1. So for example
ALTER SEQUENCE mysequence INCREMENT BY 100;
select mysequence.nextval from dual;
ALTER SEQUENCE mysequence INCREMENT BY 1;
In my case I have a sequence called PS_LOG_SEQ which had a LAST_NUMBER = 3920.
I then imported some data from PROD to my local machine and inserted into the PS_LOG table. Production data had more than 20000 rows with the latest LOG_ID (primary key) being 20070. After importing I tried to insert new rows in this table but when saving I got an exception like this one:
ORA-00001: unique constraint (LOG.PS_LOG_PK) violated
Surely this has to do with the Sequence PS_LOG_SEQ associated with the PS_LOG table. The LAST_NUMBER was colliding with data I imported which had already used the next ID value from the PS_LOG_SEQ.
To solve that I used this command to update the sequence to the latest \ max(LOG_ID) + 1:
alter sequence PS_LOG_SEQ restart start with 20071;
This command reset the LAST_NUMBER value and I could then insert new rows into the table. No more collision. :)
Note: this alter sequence command is new in Oracle 12c.
Note: this blog post documents the ALTER SEQUENCE RESTART option does exist, but as of 18c, is not documented; it is apparently intended for internal Oracle use.
These two procedures let me reset the sequence and reset the sequence based on data in a table (apologies for the coding conventions used by this client):
CREATE OR REPLACE PROCEDURE SET_SEQ_TO(p_name IN VARCHAR2, p_val IN NUMBER)
AS
l_num NUMBER;
BEGIN
EXECUTE IMMEDIATE 'select ' || p_name || '.nextval from dual' INTO l_num;
-- Added check for 0 to avoid "ORA-04002: INCREMENT must be a non-zero integer"
IF (p_val - l_num - 1) != 0
THEN
EXECUTE IMMEDIATE 'alter sequence ' || p_name || ' increment by ' || (p_val - l_num - 1) || ' minvalue 0';
END IF;
EXECUTE IMMEDIATE 'select ' || p_name || '.nextval from dual' INTO l_num;
EXECUTE IMMEDIATE 'alter sequence ' || p_name || ' increment by 1 ';
DBMS_OUTPUT.put_line('Sequence ' || p_name || ' is now at ' || p_val);
END;
CREATE OR REPLACE PROCEDURE SET_SEQ_TO_DATA(seq_name IN VARCHAR2, table_name IN VARCHAR2, col_name IN VARCHAR2)
AS
nextnum NUMBER;
BEGIN
EXECUTE IMMEDIATE 'SELECT MAX(' || col_name || ') + 1 AS n FROM ' || table_name INTO nextnum;
SET_SEQ_TO(seq_name, nextnum);
END;
If you can count on having a period of time where the table is in a stable state with no new inserts going on, this should do it (untested):
DECLARE
last_used NUMBER;
curr_seq NUMBER;
BEGIN
SELECT MAX(pk_val) INTO last_used FROM your_table;
LOOP
SELECT your_seq.NEXTVAL INTO curr_seq FROM dual;
IF curr_seq >= last_used THEN EXIT;
END IF;
END LOOP;
END;
This enables you to get the sequence back in sync with the table, without dropping/recreating/re-granting the sequence. It also uses no DDL, so no implicit commits are performed. Of course, you're going to have to hunt down and slap the folks who insist on not using the sequence to populate the column...
With oracle 10.2g:
select level, sequence.NEXTVAL
from dual
connect by level <= (select max(pk) from tbl);
will set the current sequence value to the max(pk) of your table (i.e. the next call to NEXTVAL will give you the right result); if you use Toad, press F5 to run the statement, not F9, which pages the output (thus stopping the increment after, usually, 500 rows).
Good side: this solution is only DML, not DDL. Only SQL and no PL-SQL.
Bad side : this solution prints max(pk) rows of output, i.e. is usually slower than the ALTER SEQUENCE solution.
Today, in Oracle 12c or newer, you probably have the column defined as GENERATED ... AS IDENTITY, and Oracle takes care of the sequence itself.
You can use an ALTER TABLE Statement to modify "START WITH" of the identity.
ALTER TABLE tbl MODIFY ("ID" NUMBER(13,0) GENERATED BY DEFAULT ON NULL AS IDENTITY MINVALUE 1 MAXVALUE 9999999999999999999999999999 INCREMENT BY 1 START WITH 3580 NOT NULL ENABLE);
In my case I used an approach to reset sequence to zero and than setting from zero to max of target table:
DECLARE
last_val NUMBER;
next_val NUMBER;
BEGIN
SELECT MAX(id_field) INTO next_val FROM some_table;
IF next_val > 0 THEN
SELECT some_table_seq.nextval INTO last_val FROM DUAL;
EXECUTE IMMEDIATE 'ALTER SEQUENCE some_table_seq INCREMENT BY -' || last_val || ' MINVALUE 0';
SELECT some_table_seq.nextval INTO last_val FROM DUAL;
EXECUTE IMMEDIATE 'ALTER SEQUENCE some_table_seq INCREMENT BY ' || next_val;
SELECT some_table_seq.nextval INTO last_val FROM DUAL;
EXECUTE IMMEDIATE 'ALTER SEQUENCE some_table_seq INCREMENT BY 1 MINVALUE 1';
END IF;
END;
Apologies for not having a one-liner solution, since my program runs in Typeorm with node-oracle. However, I think the following SQL commands would help with this problem.
Get the LAST_NUMBER from your sequence.
SELECT SEQUENCE_NAME, LAST_NUMBER FROM ALL_SEQUENCES WHERE SEQUENCE_NAME = '${sequenceName}'
Get the value of your PK from the last row (in this case ID is the PK).
SELECT ID FROM ${tableName} ORDER BY ID DESC FETCH NEXT 1 ROWS ONLY
Lastly update LAST_NUMBER to the value + 1:
ALTER SEQUENCE ${sequenceName} RESTART START WITH ${value + 1}