5

I have written a program where i can do a Request for an identification card.

There are different types of identification cards ( Red, Blue, Green )

While the request, the program should generate identification numbers. The numbers (Range of the numbers) depends on which card are requested.

Red Card: 1 - 50000 
Blue Card: 50001 - 100000 
Green Card: 100001 - 150000

If i add new identification cards to the system so the sequence should automatically create a new Range of numbers for that new added identification card. The Numbers should not recur. One Number should only be used one time.

How can i do that? Can anyone help me with that?

5
  • 1
    what should happen on the 50001st request for a Red Card? Commented Sep 23, 2013 at 8:40
  • 5
    Does the solution need to be concurrent, or can we assume that database requests are serialized by the app layer? Ie can we assume the application layer is single-threaded as far as the database is concerned? Commented Sep 23, 2013 at 10:21
  • 1
    Are you assuming that you will never have over 50k cards of any given type and that you will never have more cards than (MAXIMUM_INT_VALUE)/50k? Commented Sep 24, 2013 at 14:02
  • Can we create just one extra table to hold all card colors and ranges? Commented Sep 24, 2013 at 16:10
  • See also stackoverflow.com/questions/7238816/… Commented Sep 29, 2013 at 20:36

10 Answers 10

2

You can use instead of insert trigger for this

create table Cards_Types (Color nvarchar(128) primary key, Start int);
create table Cards (ID int primary key, Color nvarchar(128));

insert into Cards_Types
select 'RED', 0 union all
select 'BLUE', 50000 union all
select 'GREEN', 100000;

create trigger utr_Cards_Insert on Cards
instead of insert as
begin
    insert into Cards (id, Color)
    select
        isnull(C.id, CT.Start) + row_number() over(partition by i.Color order by i.id),
        i.Color
    from inserted as i
        left outer join Cards_Types as CT on CT.Color = i.Color
        outer apply (
            select max(id) as id
            from Cards as C
            where C.Color = i.Color
        ) as C
end

sql fiddle demo

It allows you to insert many rows at once:

insert into Cards (Color)
select 'GREEN' union all
select 'GREEN' union all
select 'RED' union all
select 'BLUE'

Note that you'd better have index on Cards columns Color, ID.

Also note that your way you can insert only 50000 records for each type. You can use different seeds, for example 1 for 'RED', 2 for 'BLUE' and so on, and reserve place for , for example, 100 types of cards:

create table Cards_Types (Color nvarchar(128) primary key, Start int);
create table Cards (ID int primary key, Color nvarchar(128));

insert into Cards_Types
select 'RED', 1 union all
select 'BLUE', 2 union all
select 'GREEN', 3;

create trigger utr_Cards_Insert on Cards
instead of insert as
begin
    insert into Cards (id, Color)
    select
        isnull(C.id, CT.Start - 100) + row_number() over(partition by i.Color order by i.id) * 100,
        i.Color
    from inserted as i
        left outer join Cards_Types as CT on CT.Color = i.Color
        outer apply (
            select max(id) as id
            from Cards as C
            where C.Color = i.Color
        ) as C
end;

sql fiddle demo

this way ID for 'RED' will always ends on 1, ID for 'BLUE' ends on 2 and so on.

Sign up to request clarification or add additional context in comments.

Comments

2

From the design perspective, I would strongly discourage coding additional logic into identifiers i.e. assigning card color to a specific range. I would rather use IDENTITY column that handles uniqueness and concurrency well, make IDs completely surrogate and store card color information for a given ID in another attribute. Possibly create an index on that additional attribute to retrieve records for a given color.

Also think about what would be needed if an owner of, say, red card requested to change it to a blue one? With ranges, to retain colors assignment you would need to create a new id and perhaps store somewhere else the information about old-to-new id sequence. What if someone changes it multiple times? With surrogate ID you can simply have one ID all the time to be able to track the same person through whole history and perhaps just add date information to your table to order changes sequentially. And this is just an example of a simple scenario.

Comments

1

You could leverage SQL Server's IDENTITY mechanism for this, because it's easy to use and handles concurrency well.

More specifically, you could create three tables that contain only an identity (auto-incremented) Id column, using this script:

create table RedCardIds(Id int identity(1, 1) primary key)
create table BlueCardIds(Id int identity(50001, 1) primary key)
create table GreenCardIds(Id int identity(100001, 1) primary key)
GO

The three tables have their identity values set to match your interval lower bounds.

Then, for every request you'd make an insert into the appropriate table and use the OUTPUT clause to get the newly generated identity value.

For example, if the request was for a Red Card, you could write:

insert RedCardIds 
output inserted.Id
default values

which would output:

Id
-----------
1

(1 row(s) affected)

On the next run it'll return 2, and so on.

Similarly, the first request for a Blue Card would trigger the statement:

insert BlueCardIds 
output inserted.Id
default values

with the result:

Id
-----------
500001

(1 row(s) affected)

4 Comments

I think to create tables isn't a good solutions for me. The Cards can be created dynamically. So there are not only the three cards. In my system i can create new cards so i need a procedure/sequence that also create automatically a new range for the new inserted card
@Paks Tables can also be created/dropped dynamically, when cards are added/deleted.
LoL, they can but the definitely shouldn't be :D
I like it. The simplest thing that could possibly work. Obvious at a glance.
0
+100

Edit #1: I updated trigger (IF UPDATE), stored procedure and last two examples.

CREATE TABLE dbo.CustomSequence
(
    CustomSequenceID INT IDENTITY(1,1) PRIMARY KEY,
    SequenceName NVARCHAR(128) NOT NULL, -- or SYSNAME
        UNIQUE(SequenceName),
    RangeStart INT NOT NULL,
    RangeEnd INT NOT NULL,
        CHECK(RangeStart < RangeEnd),
    CurrentValue INT NULL,
        CHECK(RangeStart <= CurrentValue AND CurrentValue <= RangeEnd)
);
GO
CREATE TRIGGER trgIU_CustomSequence_VerifyRange
ON dbo.CustomSequence
AFTER INSERT, UPDATE
AS
BEGIN
     IF (UPDATE(RangeStart) OR UPDATE(RangeEnd)) AND EXISTS
    (
        SELECT  *
        FROM    inserted i 
        WHERE   EXISTS
        (
            SELECT  * FROM dbo.CustomSequence cs 
            WHERE   cs.CustomSequenceID <> i.CustomSequenceID
            AND     i.RangeStart <= cs.RangeEnd
            AND     i.RangeEnd >= cs.RangeStart
        )
    )
    BEGIN
        ROLLBACK TRANSACTION;
        RAISERROR(N'Range overlapping error', 16, 1);
    END
END;
GO
--TRUNCATE TABLE dbo.CustomSequence
INSERT  dbo.CustomSequence (SequenceName, RangeStart, RangeEnd)
SELECT  N'Red Card',        1,  50000 UNION ALL
SELECT  N'Blue Card',   50001, 100000 UNION ALL
SELECT  N'Green Card', 100001, 150000;
GO
-- Test for overlapping range
INSERT  dbo.CustomSequence (SequenceName, RangeStart, RangeEnd)
VALUES  (N'Yellow Card', -100, +100);
GO
/*
Msg 50000, Level 16, State 1, Procedure trgIU_CustomSequence_VerifyRange, Line 20
Range overlapping error
Msg 3609, Level 16, State 1, Line 1
The transaction ended in the trigger. The batch has been aborted.
*/
GO

-- This procedure tries to reserve 
CREATE PROCEDURE dbo.SequenceReservation
(
    @CustomSequenceID INT, -- You could use also @SequenceName 
    @IDsCount INT, -- How many IDs do we/you need ? (Needs to be greather than 0)
    @LastID INT OUTPUT
)   
AS
BEGIN
    DECLARE @StartTranCount INT, @SavePoint VARCHAR(32);
    SET @StartTranCount = @@TRANCOUNT;
    IF @StartTranCount = 0 -- There is an active transaction ?
    BEGIN
        BEGIN TRANSACTION -- If not then it starts a "new" transaction
    END
    ELSE -- If yes then "save" a save point -- see http://technet.microsoft.com/en-us/library/ms188378.aspx
    BEGIN
        DECLARE @ProcID INT, @NestLevel INT;
        SET @ProcID = @@PROCID;
        SET @NestLevel = @@NESTLEVEL;
        SET @SavePoint = CONVERT(VARCHAR(11), @ProcID) + ',' + CONVERT(VARCHAR(11), @NestLevel);
        SAVE TRANSACTION @SavePoint;
    END

    BEGIN TRY
        UPDATE  dbo.CustomSequence
        SET     @LastID = CurrentValue = ISNULL(CurrentValue, 0) + @IDsCount
        WHERE   CustomSequenceID = @CustomSequenceID;

        IF @@ROWCOUNT = 0
            RAISERROR(N'Invalid sequence', 16, 1);

        COMMIT TRANSACTION;
    END TRY
    BEGIN CATCH
        IF @StartTranCount = 0
        BEGIN
            ROLLBACK TRANSACTION;
        END
        ELSE -- @StartTranCount > 0
        BEGIN
            ROLLBACK TRANSACTION @SavePoint
        END

        DECLARE @ErrorMessage NVARCHAR(2048), @ErrorSeverity INT, @ErrorState INT;
        SELECT @ErrorMessage = ERROR_MESSAGE(), @ErrorSeverity = ERROR_SEVERITY(), @ErrorState = ERROR_STATE();
        RAISERROR (@ErrorMessage, @ErrorSeverity, @ErrorState); 
    END CATCH;
END;
GO

SELECT * FROM dbo.CustomSequence;
GO

-- Example usage #1
DECLARE @LastID INT;
EXEC dbo.SequenceReservation  
        @CustomSequenceID = 1, -- Red Card
        @IDsCount = 2, -- How many IDs ?
        @LastID = @LastID OUTPUT;
SELECT @LastID - 2 + 1 AS [FirstID], @LastID AS [LastID];
GO

-- Example usage #2
DECLARE @LastID INT;
EXEC dbo.SequenceReservation  
        @CustomSequenceID = 1, -- Red Card
        @IDsCount = 7, -- How many IDs ?
        @LastID = @LastID OUTPUT;
SELECT @LastID - 7 + 1 AS [FirstID], @LastID AS [LastID];

SELECT * FROM dbo.CustomSequence;
GO

Results:

CustomSequenceID SequenceName RangeStart  RangeEnd    CurrentValue
---------------- ------------ ----------- ----------- ------------
1                Red Card     1           50000       9
2                Blue Card    50001       100000      NULL
3                Green Card   100001      150000      NULL

2 Comments

why output parameter called @FromID? it returns current value of sequence - sqlfiddle.com/#!3/56f4d/2
why do you need all these transactions, you have single statement which would be in transaction anyway (if implicit_tranactions are on). A bit of explanation about how OP should get actual IDs after calling procedure would be good also. At the moment code looks really overcomplicated for me, and it doesn't show actual insert of the card into Cards table
0

Ideally you would have to maintain a table to store this information.

CardCategry MinNumber MaxNumber RunningNumber

Then you can write a SP to get next number and pass card category as a parameter. A sample query would be as follows.

SELECT @count=count(RunningNumber)
FROM IdentificationTable
WHERE CardCategry=@param

IF (@count=1)
    SELECT @RunningNumber=RunningNumber
    FROM IdentificationTable
    WHERE CardCategry=@param
ELSE
    SELECT TOP 1 @min=MinNumber,@max=MaxNumber
    FROM IdentificationTable
    ORDER BY MinNumber DESC

    INSERT INTO IdentificationTable VALUES (@param,@max+1,@max+(@max-@min),1)
    SET @RunningNumber=1

RETURN @RunningNumber

This is not a complete work.Obviously you would have to do some error handling for check boundary limit etc.

Comments

0

I would try something like this:

declare @cat2start int = 50000
declare @cat3start int = 100000

declare @catName varchar(10) = 'Red'

if @catName = 'Green'
    begin
    select (max(cardnumber) + 1) as [This is the next number]
    from yourTable 
    where 
    cardnumber < @cat2start
end
if @catName = 'Blue'
    begin
    select (max(cardnumber) + 1) as [This is the next number]
    from yourTable 
    where 
    cardumber >= @cat2start and cardnumber < @cat3start
end
if @catName = 'Red'
    begin
    select (max(cardnumber) + 1) as [This is the next number]
    from yourTable 
end

Comments

0

There are a lot of answers but I will add my 2 cents. Note that I'm assuming what I've written in my comment to your original post:

create table cardTypes(cardTypeName varchar(100) primary key, [50kSlot] int unique)

create table cards (identificationNumber bigint primary key);

--add slot if needed
declare @cardToBeAdded varchar(100) = 'green'
declare @tbl50kSlot table (i int)
merge into cardTypes as t
using (select @cardToBeAdded as newCard) as s
on t.[cardTypeName] = s.newCard
when not matched by target then
insert (cardTypeName, [50kSlot]) values (s.newCard, isnull((select max([50kSlot]) + 1 from cardTypes),1))
when matched then
update set [50kSlot] = [50kSlot]
output inserted.[50kSlot] into @tbl50kSlot;

declare @50kSlot int = (Select i from @tbl50kSlot)

insert into cards (identificationNumber) values (isnull(
    (select max(identificationNumber)+1 from cards where identificationNumber between ((@50kSlot-1)*50000+1) and @50kSlot*50000),
    (@50kSlot-1)*50000+1)
)

Of course you need to add some actual data to the cards table. Notice that the last query could be performed relatively fast if there is sufficiently effective index present. It may be worth to work around indexing the identificationNumber if there will be issues with performance. Consider - for example - creation of filtering index on this column if you will have a LOT of rows.

Alternatively you could keep maxInt in in cardTypes table and make a merge table slightly more complex. The downside is that if there will be some kind of error in between the queries the number will never be used so my solutions keeps sequence tight.

Comments

0

SQL Fiddle

MS SQL Server 2008 Schema Setup:

CREATE TABLE Table1
    ([color] varchar(10), [id] int)
;

INSERT INTO Table1
    ([color], [id])
VALUES
    ('Red',(select isnull(case when (max(id)/50000)%3 = 1 and 
                                max(id)%50000 = 0 then max(id)+100000 else
                                max(id) end,0)+1 
              from Table1 where color = 'Red'));

INSERT INTO Table1  ([color], [id]) VALUES  ('Red',50000);

INSERT INTO Table1
    ([color], [id])
VALUES
    ('Red',(select isnull(case when (max(id)/50000)%3 = 1 and 
                                max(id)%50000 = 0 then max(id)+100000 else
                                max(id) end,0)+1 
              from Table1 where color = 'Red'));

INSERT INTO Table1
    ([color], [id])
VALUES
    ('Blue',(select isnull(case when (max(id)/50000)%3 = 2 and 
                                max(id)%50000 = 0 then max(id)+100000 else
                                max(id) end,50000)+1 
              from Table1 where color = 'Blue'));

INSERT INTO Table1
    ([color], [id])
VALUES
    ('Green',(select isnull(case when (max(id)/50000)%3 = 0 and 
                                max(id)%50000 = 0 then max(id)+100000 else
                                max(id) end,100000)+1 
                from Table1 where color = 'Green'));

Query 1:

SELECT *
FROM Table1

Results:

| COLOR |     ID |
|-------|--------|
|   Red |      1 |
|   Red |  50000 |
|   Red | 150001 |
|  Blue |  50001 |
| Green | 100001 |

Comments

0

And here is my contribution to the challenge. Needs no extra table, should be concurrent-safe and can handle bulk updates. Might not be the fastest, but it works. It basically copies the rows to be inserted into a separate table, creates the IDs per color and finally moves everything to the destination table.

Create Trigger Trg_CreateID ON  dbo.Cards instead of insert
as
begin
  set nocount on
  -- declare a working table holding intermediate results
  declare @Tmp Table (cardID int, cardColor char(1), cardNumber char(20))

  -- copy the data to be inserted in our working table
  insert into @Tmp select * from inserted

  declare @Id int
  -- fill in the Id's once per color    
  select @Id=coalesce (max(cardID),0) from dbo.Cards where cardColor='Red'
  update @Tmp set cardID = @Id, @Id=@id+1 where cardColor='Red'

  select @Id=coalesce(max(cardID),50000) from dbo.Cards where cardColor='Blue'
  update @Tmp set cardID = @Id, @Id=@id+1 where cardColor='Blue'

  select @Id=coalesce(max(cardID),100000) from dbo.Cards where cardColor='Gree'
  update @Tmp set cardID = @Id, @Id=@id+1 where cardColor='Green'

  -- do the actual insert here
  insert into dbo.Cards select * from @tmp
end

it assumes a table Cards like this

CREATE TABLE [dbo].[Cards]
(
    [cardID] [int] NOT NULL,
    [cardColor] [char](1) NOT NULL,
    [cardNumber] [char](20) NOT NULL
) ON [PRIMARY]

I added a constraint to the cardID column to allow omitting it in insert statements

ALTER TABLE [dbo].[Cards] 
  ADD CONSTRAINT [DF_Cards_cardID] DEFAULT ((0)) FOR [cardID]

Comments

-1

*This solution works for single row inserts, concurrency for multiple inserts needs different approach. Discussed in comments for more details *


If there is no options for creating tables then you can use instead of triggers (tweak like before triggers in oracle).

Use specific conditions inside a triggers to set the range of Identity column. Here's a sample how you can implement your solution.

Table

CREATE TABLE REQUEST_TABLE(
      REQ_ID numeric(8, 0) NOT NULL,
      REQ_COLOR VARCHAR(30) NOT NULL
 ); -- I have used this sample table

Instead of Trigger

CREATE TRIGGER tg_req_seq ON REQUEST_TABLE
INSTEAD OF INSERT AS
DECLARE @REQ_ID INT
DECLARE @REQ_COLOR VARCHAR(30)
DECLARE @REQ_START INT 
BEGIN  
  SELECT @REQ_COLOR= (SELECT ISNULL(REQ_COLOR,'NA') FROM INSERTED)

  SELECT @REQ_START = (SELECT CASE WHEN @REQ_COLOR = 'Red' THEN 0
                    WHEN @REQ_COLOR = 'Blue' THEN 50000 
                    ELSE 100000 END)

  SELECT @REQ_ID = ISNULL(MAX(REQ_ID),@REQ_START)+1 FROM REQUEST_TABLE   
    WHERE REQ_COLOR = @REQ_COLOR  

  INSERT INTO REQUEST_TABLE (REQ_ID,REQ_COLOR)
   VALUES (@REQ_ID,@REQ_COLOR)
END;

Now after some insert statements

INSERT INTO REQUEST_TABLE VALUES(NULL,'Red');
INSERT INTO REQUEST_TABLE VALUES(NULL,'Red');
INSERT INTO REQUEST_TABLE VALUES(NULL,'Red');

INSERT INTO REQUEST_TABLE VALUES(NULL,'Blue');
INSERT INTO REQUEST_TABLE VALUES(NULL,'Blue');
INSERT INTO REQUEST_TABLE VALUES(NULL,'Blue');

INSERT INTO REQUEST_TABLE VALUES(NULL,'Yellow');
INSERT INTO REQUEST_TABLE VALUES(NULL,'Yellow');
INSERT INTO REQUEST_TABLE VALUES(NULL,'Yellow');

I have added the same results in SqlFiddle. Let me know if i missed something to include.

Edit

Updated Fiddle to cater flexible requirement.

10 Comments

That looks like a good solution. How can i change SELECT REQ_START = (SELECT CASE WHEN REQ_COLOR = 'Red' THEN 0 WHEN REQ_COLOR = 'Blue' THEN 50000 ELSE 100000 END), so that if i do insert a new identification card a new range gets created. Everytime i insert a new ident card a range of 50000 should be created for that card. For example: New card (Green) Range = 1000001 - 150000 and so on
@Paks if the cards range are known then you just need to modify the case statement and add your own range. And if the card is not configured then else condition will take care of that.
@Paks note that this solution works only for one-row inserts and this is not concurrent - you can add watifor '00:00:05' between select @REQ_ID ...and insert into REQUEST_TABLE... and then run two concurrent inserts of one type of card
@Steve, more than that, its' really easy to break, if there would be some delay between select @REQ_ID and insert, then it's possible to insert dupliate @REQ_ID. I don't know why OP chose this answer.
@Pratik so basically if I write answer with words "instead of trigger" it should be considered as an answer? There're plenty of people who know about triggers on SO, but I'm sure that they didn't post a solution like this because it's completely useless. I don't even sure that my solution is concurrency stable, but I've at least tested it, and I would be glad if someone will create a better one. But your solution obviously very easy to break AND working only for single row inserts. I wouldn't downvote it, but OP accepted this as an answer and I certainly don't want other people use it.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.