Generating automatic sequential alphanumeric key

  • I have Order table as follows:

    OrderID - PK - identity

    OrderType - numeric or char (?) 1,2,3 or 'A','B','C'

    OrderSeqNo - to be automatically inserted

    OrderDate - datetime

    I would like to have OrderSeqNo automatically generated by using trigger, or function, or something else, on the following format:

    A08-001

    A08-002

    B08-001

    C08-001

    C09-001

    C09-002

    etc...

    where 'A','B','C' are OrderType, 08 and 09 are year of OrderDate, and 001,... sequential numbers for each type and year.

    I can not use identity column (OrderID), since my client don't want to have "holes" in sequential numbers.

    I think that system should first find MAX of seqno for newly inserted OrderType and Year(from OrderDate), and add one, before it assembly complete new seqno.

    Creating it this way, probably also means, that deleting Orders should not be allowed, as well as changing of OrderType, or OrderDate )how to accomplish this) ?

    Please advice.

  • What do you (or they) intend to use this field for?

    Doing this kind of generation and ensuring there will be no holes can cause a lot of problems. First, it means that all insert transactions must be entirely sequential - you cannot have two insert transactions running at the same time because if the first one rolls back, you cannot go through with the second one. To accomplish this, you will need to lock the table for inserts at the beginning of an insert transaction (using one of a number of methods) and anyone else that wants to insert has to wait for the first transaction to complete before starting. This can cause blocking and performance issues pretty easily.

    Next, if you use a MAX of some sort, your inserts will have to query the table being inserted into. So, if the table gets big you will end up with a pretty nasty query happening every time you do an insert. Not to mention dealing with multi-record inserts can be a bit more complex.

    You already noticed that deletes will be a problem.

    On the other hand, you can calculate this field with information you have on-hand. I assume there is an Order Type field and a Date field. If this number is to be used for identification purposes on documents to something just for the users to query, you may want to consider just calculating it when you need it or building it in the application UI where it needs to be displayed.

  • Thanks Michael.

    I agree with you, but still my client wants to have numbering system as described. This is of course, only for end users, and this "key" has nothing with DB itself. Moreover, inserting new orders will not happen too often (maybe 200-300 per year), and it will be done only by 1 person, so I don't except concurrency issues, etc...

    Your idea for calculating this field, based on existing columns is fine, and I already have first three characters: type 'A','B' or 'C', as well as last two numbers of year from OrderDate, but how to calculate sequential number 001,002,etc... ?

    Regards, Vladimir.

  • CREATE TABLE dbo.TempOrders (

    OrderType varchar(1) NOT NULL,

    OrderYear int,

    OrderSequence int,

    OrderNumber AS ( OrderType + RIGHT(CAST(OrderYear as varchar(4)),2) + '-' + RIGHT('000' + CAST(OrderSequence as varchar),3) ),

    PRIMARY KEY ( OrderType, OrderYear, OrderSequence ))

    INSERT INTO dbo.TempOrders VALUES( 'A', 2008, 1 )

    INSERT INTO dbo.TempOrders VALUES( 'A', 2008, 2 )

    INSERT INTO dbo.TempOrders VALUES( 'B', 2008, 1 )

    INSERT INTO dbo.TempOrders VALUES( 'C', 2008, 1 )

    INSERT INTO dbo.TempOrders VALUES( 'C', 2009, 1 )

    INSERT INTO dbo.TempOrders VALUES( 'C', 2009, 2 )

    SELECT * FROM dbo.TempOrders

    GO

    --procedure to get next order sequence

    CREATE PROCEDURE dbo.GetNextOrderNumber

    @OrderType varchar(1),

    @OrderYear int,

    @OrderSequence int OUTPUT

    AS

    SET NOCOUNT ON

    BEGIN TRY

    SELECT @OrderSequence = COALESCE(MAX(OrderSequence),0) + 1

    FROM dbo.TempOrders

    WHERE OrderType = @OrderType

    AND OrderYear = @OrderYear

    END TRY

    BEGIN CATCH

    DECLARE @ErrorMessage NVARCHAR(4000),

    @ErrorSeverity INT,

    @ErrorState INT,

    @ErrorNumber INT;

    SET @ErrorMessage = ERROR_MESSAGE();

    SET @ErrorSeverity = ERROR_SEVERITY();

    SET @ErrorState = ERROR_STATE();

    SET @ErrorNumber = ERROR_NUMBER();

    RAISERROR(@ErrorMessage,@ErrorSeverity,@ErrorState);

    RETURN @ErrorNumber;

    END CATCH

    GO

    --get the next sequence for an order type by year

    DECLARE @OrderSequence int

    EXEC dbo.GetNextOrderNumber 'C', 2008, @OrderSequence OUTPUT

    SELECT @OrderSequence as SequenceForC08

    EXEC dbo.GetNextOrderNumber 'C', 2009, @OrderSequence OUTPUT

    SELECT @OrderSequence as SequenceForC09

    Not allowing holes will cause alot of problems and there are other posts explaining how you can fill the gaps. The numbering scheme alaso limits you to less than 100 years and less than 999 orders before you will run into issues.

    You might want to add a surrogate key, and set your three natural keys to be a unique constraint.

    If you are not going to allow deletes you could raise an error in an on delete trigger

    or create a secondary table that just holds the OrderType, OrderYear, OrderSequence and Foreign key it to your orders table (do not cascade delete) and it will error if you try to delete the main record since it is referenced.

  • Also see http://www.sqlteam.com/forums/topic.asp?TOPIC_ID=67923


    N 56°04'39.16"
    E 12°55'05.25"

  • Be very careful... There is nothing there that will keep 2 or more simultaneous calls to the same code from returning the same number.

    --Jeff Moden


    RBAR is pronounced "ree-bar" and is a "Modenism" for Row-By-Agonizing-Row.
    First step towards the paradigm shift of writing Set Based code:
    ________Stop thinking about what you want to do to a ROW... think, instead, of what you want to do to a COLUMN.

    Change is inevitable... Change for the better is not.


    Helpful Links:
    How to post code problems
    How to Post Performance Problems
    Create a Tally Function (fnTally)

  • I also don't see anything that would prevent "overruns"... what happens if you use 1001 of any given sequence? Either BOOM or duplication.

    --Jeff Moden


    RBAR is pronounced "ree-bar" and is a "Modenism" for Row-By-Agonizing-Row.
    First step towards the paradigm shift of writing Set Based code:
    ________Stop thinking about what you want to do to a ROW... think, instead, of what you want to do to a COLUMN.

    Change is inevitable... Change for the better is not.


    Helpful Links:
    How to post code problems
    How to Post Performance Problems
    Create a Tally Function (fnTally)

  • Thanks guys for answers.

    I am fully aware of possible problems:

    1. Overruns will never happen, meaning that client will never have more then 200 orders per year. If it will happen in the future, I can change sequence number to have four numbers

    2. Simultaneous inserting: Since Order inserts will be mainly done by only one user, it is impossible to have same number, but still I don't understand how two users can create their inserts exactly on the same time. Is it that server will always take one by one ? What if I am using trigger on insert (see below) ?

    CREATE TRIGGER [cbs].[CreateOrderSeqNo]

    ON [cbs].[Orders]

    AFTER INSERT

    AS

    BEGIN

    SET NOCOUNT ON;

    DECLARE @OrderSeqNo AS smallint

    SELECT @OrderSeqNo = COALESCE(MAX(OrderSeqNo),0) + 1

    FROM Orders

    WHERE OrderType = (SELECT OrderType FROM Inserted)

    AND OrderYear = (SELECT OrderYear FROM Inserted)

    UPDATE Orders

    SET Orders.OrderSeqNo = @OrderSeqNo

    FROM Orders

    INNER JOIN Inserted ON Orders.OrderID = Inserted.OrderID

    END

  • You would have to ensure you create the orders one at a time and wrap it in a transaction so that the sequence returned is unique.

    You could also implement a table with the order number information and update it / return the new sequence. However, this way may burn sequence numbers.

    It depends on how the code is written to work.

    There are many possible solutions.

  • vbugarsk (12/11/2008)


    Thanks guys for answers.

    I am fully aware of possible problems:

    1. Overruns will never happen, meaning that client will never have more then 200 orders per year. If it will happen in the future, I can change sequence number to have four numbers

    2. Simultaneous inserting: Since Order inserts will be mainly done by only one user, it is impossible to have same number, but still I don't understand how two users can create their inserts exactly on the same time. Is it that server will always take one by one ? What if I am using trigger on insert (see below) ?

    Heh... overruns will never happen... famous last words of the last guy I knew that said that. At least put in a rollover detector to reject the order with the proper error message when it does eventually happen.

    Same goes for the assumption that "inserts will be mainly done by only one user". Users can insert rows into a table simultaneously if only rowlocks are invoked especially on multi-processor boxes and triggers won't prevent it. I suppose you could throw a TABLOCKX on the table, but that's a patch, not a solution for the problem.

    I've been thinking about this problem and I believe the only way to guarantee no dupes is to build a sequence table and the correct code to update it without becoming a source of major deadlocks.

    --Jeff Moden


    RBAR is pronounced "ree-bar" and is a "Modenism" for Row-By-Agonizing-Row.
    First step towards the paradigm shift of writing Set Based code:
    ________Stop thinking about what you want to do to a ROW... think, instead, of what you want to do to a COLUMN.

    Change is inevitable... Change for the better is not.


    Helpful Links:
    How to post code problems
    How to Post Performance Problems
    Create a Tally Function (fnTally)

  • There is a workaround, or a quirk, and that is to have an auxiliary sequence table, and not use source table for sequencing.

    See http://weblogs.sqlteam.com/peterl/archive/2008/11/28/An-alternative-to-IDENTITY-column.aspx


    N 56°04'39.16"
    E 12°55'05.25"

  • I think it would be better to work like this:

    The table TemoOrders would be like this

    CREATE TABLE dbo.TempOrders (

    OrderType varchar(1) NOT NULL,

    OrderYear int,

    OrderSequence int,

    OrderNumber AS ( OrderType + RIGHT(CAST(OrderYear as varchar(4)),2) + '-' + RIGHT('000' + CAST(OrderSequence as varchar),3) ),

    PRIMARY KEY ( OrderType, OrderYear))

    Then u have to insert the order sequence 1 for each ordertypw and years u want.

    The when inserting new records into the table u can do it like this:

    BEGIN TRANSACTION

    DECLARE @ordernumber TABLE (OrderNumber VARCHAR(15)),

    UPDATE TempOrders WITH (ROWLOCK)

    SET OrderSequence = OrderSequence + 1

    OUTPUT inserted.OrderSequence INTO @ordernumber

    INSERT INTO [yourtable]

    VALUES(ordernumber,Value1,.....)

    SELECT (select top 1 OrderNumber from @ordernumber), @parValue1, ....)

    COMMIT TRANSACTION

    This way u minimize database size, u can use rowlock in temp orders since the transaction will lock the temporder row for a short amount of time and since its a transaction the update wont occur if the rest of the statement fails. I don't recommend triggers, u should avoid it and control the stuff in the applications. Create stored procedures for inserts and control the use of it in the applications. In the sps do all the calculations before the transaction and use variables to reduce the lock time. Since u need to know the order number in the statement calling the sp return int in a output parameter of the spm if u can, return the entire row to the application whith a select * from yourtable. This way u can also control the value of the defaults, identity columns, ...

    Hope it helps

  • One other alternative would be to store a datetime field that represents the datetime when the order is entered into the system. Determining the order sequence number can then be done dynamically at any time using something along the lines of:

    ROW_NUMBER() OVER(PARTITION BY CUSTOMER_ID ORDER BY ORDER_DATE)

    Steve

    (aka smunson)

    :):):)

    Steve (aka sgmunson) 🙂 🙂 🙂
    Rent Servers for Income (picks and shovels strategy)

  • The code

    ROW_NUMBER() OVER(PARTITION BY CUSTOMER_ID ORDER BY ORDER_DATE)

    will error if someone deletes a row in the table. And since it's about document numbering, it can't repeat a number even if it doesn't give an error in the db system.

  • Skull Killer (12/12/2008)


    The code

    ROW_NUMBER() OVER(PARTITION BY CUSTOMER_ID ORDER BY ORDER_DATE)

    will error if someone deletes a row in the table. And since it's about document numbering, it can't repeat a number even if it doesn't give an error in the db system.

    Why would it ever error? This is generated at run-time, based on what is in the table at the time.

    The ugliness in this case in my mind is that the reference number would CHANGE if anyone went in and deleted a row, inserted rows out of sequence or updated the order dates. Kind of defeats the purpose of a reference number if it no longer refers to what it USED to refer to....

    I must admit I've never fully understood why these requirements are so damn popular. It's an arbitrary rule, most often enforced at great expense to the overall system, and in most cases - entirely unused and serving no point whatsoever. This is usually the point at which I tend to push back, hard, until the customer actually has a valid reason for this.

    ----------------------------------------------------------------------------------
    Your lack of planning does not constitute an emergency on my part...unless you're my manager...or a director and above...or a really loud-spoken end-user..All right - what was my emergency again?

Viewing 15 posts - 1 through 15 (of 18 total)

You must be logged in to reply to this topic. Login to reply