tags:

views:

1213

answers:

5

As in the question, how do I automatically reset an Oracle sequence's value back to 0 every year in Oracle 10g?

I'm using the sequence to generate an identifier in the format YYYY<sequence value> and the sequence value has to be reset to 0 every year.

YYYY is obtained from java and concatenated with the sequence value from Oracle. The format of the identifier can't be changed due to external 3rd party requirements. Thanks for any help in advance.

+1  A: 

I'm not sure there is a good way to do it, this isn't really what sequences are designed for. they are just purely incrementing unique numbers.

2 thoughts come to mind.

  1. At 12am on the first, reset the sequence, this is hard, because you need to make sure you beat any code.
  2. Create a sequence for each year, perhaps even have it in your code to be able to create the sequence, then dynamically call the correct sequence for the year.

I'd tend to favor option 2, as its not trying to do anything fancy and is always going to work without fail, any options trying to manipulate the sequence itself are bound to bite you.

Matthew Watson
A blend of the two solutions. Have a different sequence for each year. Create twenty years in advance as they don't take up much room.Have a synonym pointing to the current one, and a batch job which runs at 12am on the first to repoint the synonym.No code changes. Beware clocks may not be exactly in sync (eg the java tier may think it is 2010 before the DB tier) and the job may run a few seconds late. Maybe drop the synonym at 11:58 and create it at 12:02.
Gary
A: 

Sequences aren't really designed to be reset. But there are some cases where resetting a sequence is desirable, for example, when setting up test data, or merging production data back into a test environment. This type of activity is not normally done in production.

IF this type of operation is going to be put into production, it needs to thoroughly tested. (What causes the most concern is the potential for the reset procedure to be accidentally performed at the wrong time, like, in the middle of the year.

Dropping and recreating the sequence is one approach. As an operation, it's fairly straightforward as far as the SEQUENCE goes:

    DROP SEQUENCE MY_SEQ;
    CREATE SEQUENCE MY_SEQ START WITH 1 INCREMENT BY 1 MINVALUE 0;

[EDIT] As Matthew Watson correctly points out, every DDL statement (such as a DROP, CREATE, ALTER) will cause an implicit commit. [/EDIT]

But, any privileges granted on the SEQUENCE will be dropped, so those will need to be re-granted. Any objects that reference the sequence will be invalidated. To get this more generalized, you would need to save privileges (before dropping the sequence) and then re-grant them.

A second approach is to ALTER an existing SEQUENCE, without dropping and recreating it. Resetting the sequence can be accomplished by changing the INCREMENT value to a negative value (the difference between the current value and 0), and then do exactly one .NEXTVAL to set the current value to 0, and then change the INCREMENT back to 1. I've used a this same approach before (manually, in a test environment), to set a sequence to a larger value as well.

Of course, for this to work correctly, you need to insure no other sessions reference the sequence while this operation is being performed. An extra .NEXTVAL at the wrong instant will screw up the reset. (NOTE: achieving that on the database side is going to be difficult, if the application is connecting as the owner of the sequence, rather than as a separate user.)

To have it happen every year, you'd need to schedule a job. The sequence reset will have to be coordinated with the reset of the YYYY portion of your identifier.

Here's an example:

http://www.jaredstill.com/content/reset-sequence.html

[EDIT]

UNTESTED placeholder for one possible design of a PL/SQL block to reset sequence

    declare
      pragma autonomous_transaction;
      ln_increment       number;
      ln_curr_val        number;
      ln_reset_increment number;
      ln_reset_val       number;
    begin

      -- save the current INCREMENT value for the sequence
      select increment_by
        into ln_increment
        from user_sequences
       where sequence_name = 'MY_SEQ';

      -- determine the increment value required to reset the sequence
      -- from the next fetched value to 0
      select -1 - MY_SEQ.nextval into ln_reset_increment from dual;

      -- fetch the next value (to make it the current value)
      select MY_SEQ.nextval into ln_curr from dual;

      -- change the increment value of the sequence to 
      EXECUTE IMMEDIATE 'alter sequence MY_SEQ increment by '
        || ln_reset_increment ||' minvalue 0';

      -- advance the sequence to set it to 0
      select MY_SEQ.nextval into ln_reset_val from dual;

      -- set increment back to the previous(ly saved) value
      EXECUTE IMMEDIATE 'alter sequence MY_SEQ increment by '
        || ln_increment ;
    end;
    /

NOTES:

  • how to best protect the sequence from access while it's being reset, RENAME it?
  • Several test cases to work through here.
  • First pass, check normative cases of positive, ascending, increment 1 sequence.
  • would a better approach be to create new SEQUENCE, add permissions, rename existing and new sequences, and then re-compile dependencies?
spencer7593
CAREFUL!! Doing DML will do an implicate commit.. if you need to rollback your transaction, you wont be able to after running through this.
Matthew Watson
Interesting. But as a good practice, I tend to have my sequences local to the schema they belong to, by which I mean that they are only used in triggers/functions/procedures/packages in the same schema. Thus I don't have permissions related problems, and the DROP/CREATE solution works fine. If I need a sequence value outside the schema, I can always use a custom function/procedure.Plus, this is portable (granted, this is useless most of the time).
Mac
correction: DDL does a commit, DML does not commit by itself. (and ALTER SEQUENCE is DDL, so yes this is a problem here if someone tries to use this as part of their normal transaction code!)
Jeffrey Kemp
Sorry, I meant DDL of course.
Matthew Watson
@Mac: excellent points. Our normative pattern is for an OWNER schema (owns all of the objects) and an APP user (has privileges on OWNER objects). The Hibernate/JPA developers want direct access to the SEQUENCE, rather than through a function or trigger. Revoking privileges from APP et al. is sufficient to guarantee no access to it.
spencer7593
@spencer7563 - That is something that the OP needs to know if they are going to implement this solution. If the op is silly enough to just take this code and drop it in the middle of an application then suddenly they have potential for corrupt data.
Matthew Watson
A: 

First of all, it doesn't seem to be a way to make the sequence restart automatically every year. Read this for reference:

http://www.psoug.org/reference/OLD/sequences.html?PHPSESSID=5949da378678fa6d24b6fcc6eaae9888

My approach will be:

  1. create a table with the year and the starting sequence for that year (lets call this table year_seed)

  2. create a procedure that receives the year, checks the year_seed table and if it's the first check for the year generates the register with the starting sequence. This procedure must also return the sequence minus the starting sequence for the year.

Maybe it's not so simple but I think it's the best solution. Good luck

tekBlues
Actually, the referenced link DOES show examples of how to reset a sequence, including a stored procedure.
spencer7593
Yes it does, but as far as I can see there's no way to make it restart automatically each year.
tekBlues
+1  A: 

Use a job to do the trick. First, create a stored procedure to reset your sequence (I usually go with the DROP/CREATE solution, but you could use spencer7593's trick) :

CREATE OR REPLACE PROCEDURE my_seq_reset AS
BEGIN
    EXECUTE IMMEDIATE 'DROP SEQUENCE my_seq';
    EXECUTE IMMEDIATE
      'CREATE SEQUENCE my_seq' ||
      '  MINVALUE 1 ' ||
      '  MAXVALUE 999999 ' ||
      '  START WITH 1 ' ||
      '  INCREMENT BY 1 ' ||
      '  NOCACHE';
END;

Then create the job (see here for the reference) :

BEGIN
  dbms_scheduler.create_job(
    job_name        => 'job$my_seq_reset',
    job_type        => 'STORED_PROCEDURE',
    job_action      => 'my_seq_reset',
    start_date      => TO_DATE('01-01-09', 'DD-MM-RR'),
    repeat_interval => 'FREQ=YEARLY;BYDATE=0101',
    enabled         => TRUE,
    auto_drop       => FALSE,
    comments        => 'My sequence yearly reset job.'
  );
END;

You're done.

Mac
+2  A: 

Just throwing this out there as an idea:

If you want a solution that requires no ongoing DDL (i.e. no dropping and creating or resetting sequences), or even any jobs, you could consider something like this (this is in principle only, I haven't tested this approach but I'm sure it'll work):

  1. Create a single sequence.

  2. Create a reference table, with one row for each year, e.g.

    YEARS (year NUMBER(4,0) PRIMARY KEY, starting_value NUMBER)

  3. When you get NEXTVAL from the sequence, you then have to subtract the starting_value when queried from the YEARS table for the current year. If the year is not found, a new row should be inserted (i.e. the first process run in any given year will insert the new value).

e.g. a function, e.g. get_year_starting_value (pn_year IN NUMBER) RETURN NUMBER could query this table and return the starting_value for the given year; if it gets NO_DATA_FOUND, it could call a procedure to insert it using the NEXTVAL from the sequence (committed in an autonomous transaction so that the new value is immediately available to other sessions, and so that the function doesn't fail due to the side effect)

Probably not a solution for all cases, but I think this approach may help in at least some scenarios.

Jeffrey Kemp
I don't think there's an answer to the question as asked: you can't do it automatically - sequences aren't intended to be used that way. This seems to be a way to achieve most of what's actually needed. I think I'd be inclined to stop trying to use a sequence at all.
Mike Woodhouse
@Jeffrey, seems like a workable approach. I'm concerned about that obscure corner case, when two separate transactions "discover" the missing year row from the table, it seems like serializing access to the table is the sure way to prevent it (but sequences are all about reducing the performance impact of serialized access to resources in the first place). I'm not certain an autonomous transaction fixes the problem. An opportunistic locking strategy may work, we only have to deal with the exception cases when no row for the year is found, and prevent rows from being deleted.
spencer7593
Yes. The primary key constraint on the YEARS table would effectively serialise access to the table, and this would only happen once a year - the procedure would need to handle the DUP_VAL_ON_INDEX exception appropriately.The idea of the autonomous transaction is just to avoid the commit affecting the calling transaction.
Jeffrey Kemp
+1 Woodhouse - if the application could be modified to generate the values at query time that would be preferable.
Jeffrey Kemp
+1 @Jeffrey, yes, with the unique constraint AND appropriate handling of the ORA-00001 exception, this approach would work, and is likely be preferable (in a production environment) to "resetting" the sequence. (resetting a sequence is an appropriate approach in a test environment, for merging data and setting up test cases
spencer7593