Incrementing custom primary key values in SQL - sql

I am asked to generate custom ID values for primary key columns. The query is as follows,
SELECT * FROM SC_TD_GoodsInward WHERE EntityId = #EntityId
SELECT #GoodsInwardId=IIF((SELECT COUNT(*) FROM SC_TD_GoodsInward)>0, (Select 'GI_'+RIGHT('00'+CONVERT(varchar,datepart(YY,getdate())),2)+RIGHT('00'+CONVERT(varchar,datepart(MM,getdate())),2)+RIGHT('00'+CONVERT(varchar,datepart(DD,getdate())),2)+'_'+CONVERT(varchar,#EntityId)+'_'+(SELECT RIGHT('0000'+CONVERT(VARCHAR,CONVERT(INT,RIGHT(MAX(GoodsInwardId),4))+1),4) from SC_TD_GoodsInward)), (SELECT 'GI_'+RIGHT('00'+CONVERT(varchar,datepart(YY,getdate())),2)+RIGHT('00'+CONVERT(varchar,datepart(MM,getdate())),2)+RIGHT('00'+CONVERT(varchar,datepart(DD,getdate())),2)+'_'+CONVERT(varchar,#EntityId)+'_0001'))
Here the SC_TD_GoodsInward is a table, GoodsInwardId is the value to be generated. I am getting the desired outputs too. Examples.
GI_131118_1_0001
GI_131212_1_0002
GI_131212_1_0003
But, the above condition fails when the last digits reach 9999. I simulated the query and the results were,
GI_131226_1_9997
GI_140102_1_9998
GI_140102_1_9999
GI_140102_1_0000
GI_140102_1_0000
GI_140102_1_0000
GI_140102_1_0000
GI_140102_1_0000
After 9999, it goes to 0000 and does not increment thereafter. So, in the future, I will eventually run into a PK duplicate error. How can i recycle the values so that after 9999, it goes on as 0000, 0001 ... etc. What am I missing in the above query?
NOTE: Please consider the #EntityId value to be 1 in the query.
I am using SQL SERVER 2012.

Before giving a solution for the question few points on your question:
As the Custom primary key consists of mainly three parts Date(140102), physical location where transaction takes place (entityID), 4 place number(9999).
According to the design on a single date in a single physical location there cannot be more than 9999 transactions -- My Solution will also contain the same limitation.
Some points on my solution
The 4 place digit is tied up to the date which means for a new date the count starts from 0000. For Example
GI_140102_1_0001,
GI_140102_1_0002,
GI_140102_1_0003,
GI_140103_1_0000,
GI_140104_1_0000
Any way the this field will be unique.
The solution compares the latest date in the record to the current date.
The Logic:
If current date and latest date in the record matches
Then it increments 4 place digit by the value by 1
If the current date and the latest date in the record does not matched
The it sets the 4 place digit by the value 0000.
The Solution: (Below code gives out the value which will be the next GoodsInwardId, Use it as per requirement to fit in to your solution)
declare #previous nvarchar(30);
declare #today nvarchar(30);
declare #newID nvarchar(30);
select #previous=substring(max(GoodsInwardId),4,6) from SC_TD_GoodsInward;
Select #today=RIGHT('00'+CONVERT(varchar,datepart(YY,getdate())),2)
+RIGHT('00'+CONVERT(varchar,datepart(MM,getdate())),2)+RIGHT('00'+CONVERT(varchar,datepart(DD,getdate())),2);
if #previous=#today
BEGIN
Select #newID='GI_'+RIGHT('00'+CONVERT(varchar,datepart(YY,getdate())),2)
+RIGHT('00'+CONVERT(varchar,datepart(MM,getdate())),2)+RIGHT('00'+CONVERT(varchar,datepart(DD,getdate())),2)
+'_'+CONVERT(varchar,1)+'_'+(SELECT RIGHT('0000'+
CONVERT(VARCHAR,CONVERT(INT,RIGHT(MAX(GoodsInwardId),4))+1),4)
from SC_TD_GoodsInward);
END
else
BEGIN
SET #newID='GI_'+RIGHT('00'+CONVERT(varchar,datepart(YY,getdate())),2)
+RIGHT('00'+CONVERT(varchar,datepart(MM,getdate())),2)+RIGHT('00'+CONVERT(varchar,datepart(DD,getdate())),2)
+'_'+CONVERT(varchar,1)+'_0000';
END
select #newID;
T-SQL to create the required structure (Probable Guess)
For the table:
CREATE TABLE [dbo].[SC_TD_GoodsInward](
[EntityId] [int] NULL,
[GoodsInwardId] [nvarchar](30) NULL
)
Sample records for the table:
insert into dbo.SC_TD_GoodsInward values(1,'GI_140102_1_0000');
insert into dbo.SC_TD_GoodsInward values(1,'GI_140101_1_9999');
insert into dbo.SC_TD_GoodsInward values(1,'GI_140101_1_0001');
**Its a probable solution in your situation although the perfect solution would be to have identity column (use reseed if required) and tie it with the current date as a computed column.

You get this problem because once the last 4 digits reach 9999, 9999 will remain the highest number no matter how many rows are inserted, and you are throwing away the most significant digit(s).
I would remodel this to track the last used INT portion value of GoodsInwardId in a separate counter table (as an INTEGER), and then MODULUS (%) this by 10000 if need be. If there are concurrent calls to the PK generator, remember to lock the counter table row.
Also, even if you kept all the digits (e.g. in another field), note that ordering a CHAR is as follows
1
11
2
22
3
and then applying MAX() will return 3, not 22.
Edit - Clarification of counter table alternative
The counter table would look something like this:
CREATE TABLE PK_Counters
(
TableName NVARCHAR(100) PRIMARY KEY,
LastValue INT
);
(Your #EntityID might be another candidate for the counter PK column.)
You then increment and fetch the applicable counter on each call to your custom PK Key generation PROC:
UPDATE PK_Counters
SET LastValue = LastValue + 1
WHERE TableName = 'SC_TD_GoodsInward';
Select
'GI_'+RIGHT('00'+CONVERT(varchar,datepart(YY,getdate())),2)
+RIGHT('00'+CONVERT(varchar,datepart(MM,getdate())),2)
+RIGHT('00'+CONVERT(varchar,datepart(DD,getdate())),2)+'_'
+CONVERT(varchar,#EntityId)+'_'
+(SELECT RIGHT('0000'+ CONVERT(NVARCHAR, LastValue % 10000),4)
FROM PK_Counters
WHERE TableName = 'SC_TD_GoodsInward');
You could also modulo the LastValue in the counter table (and not in the query), although I believe there is more information about the number of records inserted by leaving the counter un-modulo-ed.
Fiddle here
Re : Performance - Selecting a single integer value from a small table by its PK and then applying modulo will be significantly quicker than selecting MAX from a SUBSTRING (which would almost certainly be a scan)

DECLARE #entityid INT = 1;
SELECT ('GI_'
+ SUBSTRING(convert(varchar, getdate(), 112),3,6) -- yymmdd today DATE
+ '_' + CAST(#entityid AS VARCHAR(50)) + '_' --#entity parameter
+ CASE MAX(t.GI_id + 1) --take last number + 1
WHEN 10000 THEN
'0000' --reset
ELSE
RIGHT( CAST('0000' AS VARCHAR(4)) +
CAST(MAX(t.GI_id + 1) AS VARCHAR(4))
, 4)
END) PK
FROM
(
SELECT TOP 1
CAST(SUBSTRING(GoodsInwardId,11,1) AS INT) AS GI_entity,
CAST(SUBSTRING(GoodsInwardId,4,6) AS INT) AS GI_date,
CAST(RIGHT(GoodsInwardId,4) AS INT) AS GI_id
FROM SC_TD_GoodsInward
WHERE CAST(SUBSTRING(GoodsInwardId,11,1) AS INT) = #entityid
ORDER BY gi_date DESC, rowTimestamp DESC, gi_id DESC
) AS t
This should take the last GoodInwardId record, ordered by date DESC and take its numeric "id". Then add + 1 to return the NEW id and combine it with today's date and the #entityid you passed. If >9999, start again from 0000.
You need a timestamp type column tho, to order two inserted in the same date + same transaction time. Otherwise you could get duplicates.

I have simplified the answer even more and arrived with the following query.
IF (SELECT COUNT(GoodsInwardId) FROM SC_TD_GoodsInward WHERE EntityId = #EntityId)=0
BEGIN
SELECT #GoodsInwardId= 'GI_'+RIGHT('00'+CONVERT(varchar,datepart(YY,getdate())),2)+
RIGHT('00'+CONVERT(varchar,datepart(MM,getdate())),2)+
RIGHT('00'+CONVERT(varchar,datepart(DD,getdate())),2)+'_'+
CONVERT(varchar,#EntityId)+'_0001'
END
ELSE
BEGIN
SELECT * FROM SC_TD_GoodsInward WHERE EntityId = #EntityId AND CONVERT(varchar,CreatedOn,103) = CONVERT(varchar,GETDATE(),103)
SELECT #GoodsInwardId=IIF(##ROWCOUNT>0,
(Select 'GI_'+
RIGHT('00'+CONVERT(varchar,datepart(YY,getdate())),2)+
RIGHT('00'+CONVERT(varchar,datepart(MM,getdate())),2)+
RIGHT('00'+CONVERT(varchar,datepart(DD,getdate())),2)+'_'+
CONVERT(varchar,#EntityId)+'_'+
(SELECT RIGHT('0000'+CONVERT(VARCHAR,CONVERT(INT,RIGHT(MAX(GoodsInwardId),4))+1),4) from SC_TD_GoodsInward WHERE CONVERT(varchar,CreatedOn,103) = CONVERT(varchar,GETDATE(),103))),
(SELECT 'GI_'+RIGHT('00'+CONVERT(varchar,datepart(YY,getdate())),2)+
RIGHT('00'+CONVERT(varchar,datepart(MM,getdate())),2)+
RIGHT('00'+CONVERT(varchar,datepart(DD,getdate())),2)+'_'+
CONVERT(varchar,#EntityId)+'_0001'))
END
select * from SC_TD_GoodsInward

Related

How to create a deterministic uniqueidentifier (GUID) from an integer value

Note: This is not about database design nor about the general use of a GUID. This is about deterministically create such GUID's for test data, on a Microsoft SQL server.
We are migrating our database away from integer identifiers to the uniqueidentifier data type.
For test purposes we want to migrate our test data sets to known GUID values, deterministically based on our former integer values
UPDATE Unit
SET UnitGuid = NEWID(UnitId)
Obviously this does not work right away. How to use the UnitId to create a deterministic GUID?
You could create keymap table:
CREATE TABLE tab_map(id_old INT PRIMARY KEY, guid UNIQUEIDENTIFIER);
INSERT INTO tab_map(id_old, guid)
SELECT id, NEWID()
FROM src_table;
DBFiddle Demo
After that you could use simple query or wrap with a function:
SELECT guid
FROM tab_map
WHERE id_old = ?
Stop thinking about the problem from a "string" perspective. an int is made up of 4 bytes. A uniqueidentifier is made up of 16 bytes. you can easily take 12 fixed bytes and append the four bytes from an int to the end of those, and get a solution that works for all int values:
declare #Unit table
(
UniqueColumn UNIQUEIDENTIFIER DEFAULT NEWID(),
Characters VARCHAR(10),
IntegerId int
)
-- Add *3* data rows
INSERT INTO #Unit(Characters, IntegerId) VALUES ('abc', 1111),('def', 2222),('ghi',-17)
-- Deterministically creates a uniqueidentifier value out of an integer value.
DECLARE #GuidPrefix binary(12) = 0xefbeadde0000000000000000
UPDATE #Unit
SET UniqueColumn = CONVERT(uniqueidentifier,#GuidPrefix + CONVERT(binary(4),IntegerId))
-- Check the result
SELECT * FROM #Unit
Result:
UniqueColumn Characters IntegerId
------------------------------------ ---------- -----------
DEADBEEF-0000-0000-0000-000000000457 abc 1111
DEADBEEF-0000-0000-0000-0000000008AE def 2222
DEADBEEF-0000-0000-0000-0000FFFFFFEF ghi -17
(For various reasons, we have to provide the first four bytes in a different order than the one that is used by default when displaying a uniqueidentifier as a string, which is why if we want to display DEADBEEF, we had to start our binary as efbeadde)
Also, of course, insert usual warnings that if you're creating guids/uniqueidentifiers but not using one of the prescribed methods for generating them, then you cannot assume any of the usual guarantees about uniqueness.
I ended up solving this for myself. Here's my solution for future reference:
I create a prefix part of the GUID in the form of deadbeef-0000-0000-0000-, then append a "stringified", zero-padded version of the Id column's integer value to it, like 000000000001, wich results in
DEADBEEF-0000-0000-0000-000000000001
in this example.
Here's the SQL command for this action on a whole table:
-- Deterministically creates a uniqueidentifier value out of an integer value.
DECLARE #GuidPrefix nvarchar(max) = N'deadbeef-0000-0000-0000-'; -- without the last 12 digits
UPDATE Unit
SET UniqueColumn =
(SELECT #GuidPrefix + RIGHT('000000000000' + CAST(IntegerId AS NVARCHAR (12)), 12 ) AS NUMBER_CONVERTED)
Warnings:
This implementation works only for positive int values (which are up
to 2147483647 max)
This is only intended for test data! Use is
strongly discouraged for production data!
And here's a complete working example:
-- Create an example table with random GUID's
CREATE TABLE Unit
(
UniqueColumn UNIQUEIDENTIFIER DEFAULT NEWID(),
Characters VARCHAR(10),
IntegerId int
)
-- Add 2 data rows
INSERT INTO Unit(Characters, IntegerId) VALUES ('abc', 1111)
INSERT INTO Unit(Characters, IntegerId) VALUES ('def', 2222)
-- Deterministically creates a uniqueidentifier value out of an integer value.
DECLARE #GuidPrefix nvarchar(max) = N'deadbeef-0000-0000-0000-'; -- without the last 12 digits
UPDATE Unit
SET UniqueColumn =
(SELECT #GuidPrefix + RIGHT('000000000000' + CAST(IntegerId AS NVARCHAR (12)), 12 ) AS NUMBER_CONVERTED)
-- Check the result
SELECT * FROM Unit
Result:
UniqueColumn Characters IntegerId
--------------------------------------- ---------- ---------
DEADBEEF-0000-0000-0000-000000001111 abc 1111
DEADBEEF-0000-0000-0000-000000002222 def 2222

Customized Primary Key on SQL Server 2008 R2

I have several days trying to solve this problem, but my lack of knowledge is stopping me, I don’t know if is possible what I am trying to accomplish.
I need to have a table like this:
The first field should be a custom primary key ID (auto incremented):
YYYYMMDD-99
Where YYYMMDD is the actual day and “99” is a counter that should be incremented automatically from 01 to 99 in every new row added and need to be automatically restarted to 01 the next day.
The second field is a regular NVARCHAR(40) text field called: Name
For example, I add three rows, just introducing the “Name” of the person, the ID is automatically added:
ID Name
---------------------------
20160629-01 John
20160629-02 Katie
20160629-03 Mark
Then, the next day I add two new rows:
ID Name
-------------------------
20160630-01 Bob
20160630-02 Dave
The last two digits should be restarted, after the day changes.
And, what is all this about ?
Answer: Customer requirement.
If is possible to do it in a stored procedure, it will works for me too.
Thanks in advance!!
This is pretty easy to achieve, but a bit complicated to do so it is safe with multiple clients.
What you need is a new table (for example named IndexHelper) that actually stores the parts of the index as it should be using two columns: One has the current date properly formatted as you want it in your index and one is the current index as integer. Example:
DateString CurrentIndex
-------------------------------
20160629 13
Now you need some code that helps you get the next index value atomically, i.e. in a way that also works when more than one client try to insert at the same time without getting the same index more than once.
T-SQL comes to the rescue with its UPDATE ... OUTPUT clause, which allows you to update a table, at the same time outputting the new values as an atomic operation, which can not be interrupted.
In your case, this statement could look like this:
DECLARE #curDay NVARCHAR(10)
DELCARE #curIndex INT
DECLARE #tempTable TABLE (theDay NVARCHAR(10), theIndex INT)
UPDATE IndexHelper SET CurrentIndex = CurrentIndex + 1 OUTPUT INSERTED.DateString, INSERTED.CurrentIndex INTO #temptable WHERE CurrentDate = <code that converts CURRENT_TIMESTAMP into the string format you want>
SELECT #curDay = theDay, #curIndex = theIndex FROM #tempTable
Unfortunately you have to go the temporary table way, as it is demanded by the OUTPUT clause.
This increments the CurrentIndex field in IndexHelper atomically for the current date. You can combine both into a value like this:
DECLARE #newIndexValue NVARCHAR(15)
SET #newIndexValue = #curDay + '-' + RIGHT('00' + CONVERT(NVARCHAR, #curIndex), 2)
Now the question is: How do you handle the "go back to 01 for the next day" requirement? Also easy: Add entries into IndexHelper for 2 days in advance with the respective date and index 0. You can do this safely everytime your code is called if you check that an entry for a day is actually missing. So for today your table might look like this:
DateString CurrentIndex
-------------------------------
20160629 13
20160630 0
20160701 0
The first call tomorrow would make this look like:
DateString CurrentIndex
-------------------------------
20160629 13
20160630 1
20160701 0
20160702 0
Wrap this up into a stored procedure that does the entire INSERT process into your original table, what you get is:
Add missing entries for the next two days to IndexHelper table.
Get the next ID atomically as described above
Combine date string and ID from the UPDATE command into a single string
Use this in the INSERT command for your actual data
This results in the following stored procedure you can use to insert your data:
-- This is our "work date"
DECLARE #now DATETIME = CURRENT_DATETIME
-- These are the date strings that we need
DECLARE #today NVARCHAR(10) = CONVERT(NVARCHAR, #now, 112)
DECLARE #tomorrow NVARCHAR(10) = CONVERT(NVARCHAR, DATEADD(dd, 1, #now), 112)
DECLARE #datomorrow NVARCHAR(10) = CONVERT(NVARCHAR, DATEADD(dd, 2, #now), 112)
-- We will need these later
DECLARE #curDay NVARCHAR(10)
DELCARE #curIndex INT
DECLARE #tempTable TABLE (theDay NVARCHAR(10), theIndex INT)
DECLARE #newIndexValue NVARCHAR(15)
-- Add entries for next two days into table
-- NOTE: THIS IS NOT ATOMIC! SUPPOSED YOU HAVE A PK ON DATESTRING, THIS
-- MAY EVEN FAIL! THAT'S WHY IS USE BEGIN TRY
BEGIN TRY
IF NOT EXISTS (SELECT 1 FROM IndexHelper WHERE DateString = #tomorrow)
INSERT INTO IndexHelper (#tomorrow, 0)
END TRY
BEGIN CATCH
PRINT 'hmpf'
END CATCH
BEGIN TRY
IF NOT EXISTS (SELECT 1 FROM IndexHelper WHERE DateString = #datomorrow)
INSERT INTO IndexHelper (#datomorrow, 0)
END TRY
BEGIN CATCH
PRINT 'hmpf again'
END CATCH
-- Now perform the atomic update
UPDATE IndexHelper
SET
CurrentIndex = CurrentIndex + 1
OUTPUT
INSERTED.DateString,
INSERTED.CurrentIndex
INTO #temptable
WHERE CurrentDate = #today
-- Get the values after the update
SELECT #curDay = theDay, #curIndex = theIndex FROM #tempTable
-- Combine these into the new index value
SET #newIndexValue = #curDay + '-' + RIGHT('00' + CONVERT(NVARCHAR, #curIndex), 2)
-- PERFORM THE INSERT HERE!!
...
One way to achieve customised auto increment is using INSTEAD OF trigger in SQL Server.
https://msdn.microsoft.com/en-IN/library/ms189799.aspx
I have tested this using below code.
This might be helpful.
It is written with the assumption that maximum 99 records will be inserted in a given day.
You will have to modify it to handle more than 99 records.
CREATE TABLE dbo.CustomerTb(
ID VARCHAR(50),
Name VARCHAR(50)
)
GO
CREATE TRIGGER dbo.InsertCustomerTrigger ON dbo.CustomerTb INSTEAD OF INSERT
AS
BEGIN
DECLARE #MaxID SMALLINT=0;
SELECT #MaxID=ISNULL(MAX(RIGHT(ID,2)),0)
FROM dbo.CustomerTb
WHERE LEFT(ID,8)=FORMAT(GETDATE(),'yyyyMMdd');
INSERT INTO dbo.CustomerTb(
ID,
Name
)
SELECT FORMAT(GETDATE(),'yyyyMMdd')+'-'+RIGHT('00'+CONVERT(VARCHAR(5),ROW_NUMBER() OVER(ORDER BY Name)+#MaxID),2),
Name
FROM inserted;
END
GO
TEST CASE 1
INSERT INTO dbo.CustomerTb(NAME) VALUES('A'),('B');
SELECT * FROM dbo.CustomerTb;
TEST CASE 2
INSERT INTO dbo.CustomerTb(NAME) VALUES('P'),('Q');
SELECT * FROM dbo.CustomerTb;

Unique 6 digit number but not sequential for Customer ID (SQL)

Our Customer table has an int Identity column for ID. This was going to be given out to customers, so when they phone they could just give their ID.
It is now obvious that our competitors would easily be able to register twice on our site, say a month apart and find out exactly how many people have registered.
Therefore, is there a nice simple way to create a "Customer ID" (in SQL or c#) which we could give to customers that is:
(a) 6 digits long
(b) is unique
(c) is not sequential(
Thanks in advance
If you choose any increment that is not a factor of 1000000, then you could take the last 6 digits of that number to provide the ID; ie (IDENTITY (1,7)) % 1000000.
But your competitors could still find the increment by a few sequential registrations, so this would not completely solve the issue.
So it would seem you want a number that is completely random - so for that, you'll have to check whether it already exists when you generate it, or pre-generate a list of numbers, sort them randomly, and pick the next when creating a new customer.
Another option to consider is some form of encryption, if you can find or create an appropriate algorithm that creates a short enough output.
If you take the large non factor increment route, you could then subsequently re-arrange the order of the digits to create a more random number - eg;
declare #inc int , #loop int
declare #t table (i int, cn int, code varchar(4))
select #inc = 5173, #loop = 1
while #loop<=10000
begin
insert #t (i, cn)
select #loop, (#inc*#loop)%10000
select #loop = #loop + 1
end
update #t
set code = substring(convert(varchar(4),cn),2,1)
+ substring(convert(varchar(4),cn),4,1)
+ substring(convert(varchar(4),cn),3,1)
+ substring(convert(varchar(4),cn),1,1)
select code, count(*) from #t group by code having count(*)>1
select top 20 * from #t order by i
Depending on the number you choose, some sequential items will have the same difference between them, but this number will vary. So it's not cryptographically secure, but probably enough to thwart all but the most determined of competitors.
You could convert the above to a function to run off a standard IDENTITY(1,1) id field
Maybe this is insane, but here is my way of generating the Customer Numbers up front.
This will generate however many UNIQUE keys you want very quickly.
You could obviously save these into a real table.
Here is a SQLFiddle of the below: http://www.sqlfiddle.com/#!3/d41d8/3884
DECLARE #tbl TABLE
(
ID INT IDENTITY(1,1),
CustNo INT UNIQUE
)
DECLARE #Upper INT
DECLARE #Lower INT
DECLARE #NumberRequired INT
SET #Lower = 100000 ---- The lowest random number allowed
SET #Upper = 999999 ---- The highest random number allowed
SET #NumberRequired = 1000 -- How many IDs do we want?
WHILE (SELECT COUNT(*) FROM #tbl) < #NumberRequired
BEGIN
BEGIN TRY
INSERT INTO #tbl SELECT (ROUND(((#Upper - #Lower -1) * RAND() + #Lower), 0))
END TRY
BEGIN CATCH
-- If it goes wrong go round the loop again
END CATCH
END
SELECT *
FROM #tbl
EDIT: Actually this is probably faster. It generates all 900000 possible keys in around 30 seconds on my dev machine, which is okay for a one-off job.
DECLARE #tbl TABLE
(
ID INT
)
DECLARE #Upper INT
DECLARE #Lower INT
DECLARE #i INT;
SET #Lower = 100000 ---- The lowest random number allowed
SET #Upper = 999999 ---- The highest random number allowed
SET #i = #Lower
WHILE #i <= #Upper
BEGIN
INSERT INTO #tbl SELECT #i
SET #i = #i + 1
END
SELECT ID
FROM #tbl ORDER BY NEWID()
You can have calculated column that generated from Identity column and create unique value that Expect.
for example calculated column like below :
100000 + Identity_Column * 7 + 3
What if juts use user registration timestamp. It doesn't contain user's count information and unique (If you don't register users each second for example). For instance if you use 10000 in this query you can register users each minute and get unique 9 symbol digit:
select cast(cast(current_timestamp as float)*10000 as int)
You could make a table with 2 columns, one with the values 100.000 to 999.999 and one with a marker whether the number has been given out. When making a new client assign an unassigned number from this table at random and mark it assigned.

sql server: generate primary key based on counter and another column value

I am creating a customer table with a parent table that is company.
It has been dictated(chagrin) that I shall create a primary key for the customer table that is a combination of the company id which is an existing varchar(4) column in the customer table, e.g. customer.company
The rest of the varchar(9) primary key shall be a zero padded counter incrementing through the number of customers within that company.
E.g. where company = MSFT and this is a first insert of an MSFT record: the PK shall be MSFT00001
on subsequent inserts the PK would be MSFT00001, MSFT00002 etc.
Then when company = INTL and its first record is inserted, the first record would be INTL00001
I began with an instead of trigger and a udf that I created from other stackoverflow responses.
ALTER FUNCTION [dbo].[GetNextID]
(
#in varchar(9)
)
RETURNS varchar(9) AS
BEGIN
DECLARE #prefix varchar(9);
DECLARE #res varchar(9);
DECLARE #pad varchar(9);
DECLARE #num int;
DECLARE #start int;
if LEN(#in)<9
begin
set #in = Left(#in + replicate('0',9) , 9)
end
SET #start = PATINDEX('%[0-9]%',#in);
SET #prefix = LEFT(#in, #start - 1 );
declare #tmp int;
set #tmp = len(#in)
declare #tmpvarchar varchar(9);
set #tmpvarchar = RIGHT( #in, LEN(#in) - #start + 1 )
SET #num = CAST( RIGHT( #in, LEN(#in) - #start + 1 ) AS int ) + 1
SET #pad = REPLICATE( '0', 9 - LEN(#prefix) - CEILING(LOG(#num)/LOG(10)) );
SET #res = #prefix + #pad + CAST( #num AS varchar);
RETURN #res
END
How would I write my instead of trigger to insert the values and increment this primary key. Or should I give it up and start a lawnmowing business?
Sorry for that tmpvarchar variable SQL server was giving me strange results without it.
Whilst I agree with the naysayers, the principle of "accepting that which cannot be changed" tends to lower the overall stress level, IMHO. Try the following approach.
Disadvantages
Single-row inserts only. You won't be doing any bulk inserts to your new customer table as you'll need to execute the stored procedure each time you want to insert a row.
A certain amount of contention for the key generation table, hence a potential for blocking.
On the up side, though, this approach doesn't have any race conditions associated with it, and it isn't too egregious a hack to really and truly offend my sensibilities. So...
First, start with a key generation table. It will contain 1 row for each company, containing your company identifier and an integer counter that we'll be bumping up each time an insert is performed.
create table dbo.CustomerNumberGenerator
(
company varchar(8) not null ,
curr_value int not null default(1) ,
constraint CustomerNumberGenerator_PK primary key clustered ( company ) ,
)
Second, you'll need a stored procedure like this (in fact, you might want to integrate this logic into the stored procedure responsible for inserting the customer record. More on that in a bit). This stored procedure accepts a company identifier (e.g. 'MSFT') as its sole argument. This stored procedure does the following:
Puts the company id into canonical form (e.g. uppercase and trimmed of leading/trailing whitespace).
Inserts the row into the key generation table if it doesn't already exist (atomic operation).
In a single, atomic operation (update statement), the current value of the counter for the specified company is fetched and then incremented.
The customer number is then generated in the specified way and returned to the caller via a 1-row/1-column SELECT statement.
Here you go:
create procedure dbo.GetNewCustomerNumber
#company varchar(8)
as
set nocount on
set ansi_nulls on
set concat_null_yields_null on
set xact_abort on
declare
#customer_number varchar(32)
--
-- put the supplied key in canonical form
--
set #company = ltrim(rtrim(upper(#company)))
--
-- if the name isn't already defined in the table, define it.
--
insert dbo.CustomerNumberGenerator ( company )
select id = #company
where not exists ( select *
from dbo.CustomerNumberGenerator
where company = #company
)
--
-- now, an interlocked update to get the current value and increment the table
--
update CustomerNumberGenerator
set #customer_number = company + right( '00000000' + convert(varchar,curr_value) , 8 ) ,
curr_value = curr_value + 1
where company = #company
--
-- return the new unique value to the caller
--
select customer_number = #customer_number
return 0
go
The reason you might want to integrate this into the stored procedure that inserts a row into the customer table is that it makes globbing it all together into a single transaction; without that, your customer numbers may/will get gaps when an insert fails land gets rolled back.
As others said before me, using a primary key with calculated auto-increment values sounds like a very bad idea!
If you are allowed to and if you can live with the downsides (see at the bottom), I would suggest the following:
Use a normal numeric auto-increment key and a char(4) column which only contains the company id.
Then, when you select from the table, you use row_number on the auto-increment column and combine that with the company id so that you have an additional column with a "key" that looks like you wanted (MSFT00001, MSFT00002, ...)
Example data:
create table customers
(
Id int identity(1,1) not null,
Company char(4) not null,
CustomerName varchar(50) not null
)
insert into customers (Company, CustomerName) values ('MSFT','First MSFT customer')
insert into customers (Company, CustomerName) values ('MSFT','Second MSFT customer')
insert into customers (Company, CustomerName) values ('ABCD','First ABCD customer')
insert into customers (Company, CustomerName) values ('MSFT','Third MSFT customer')
insert into customers (Company, CustomerName) values ('ABCD','Second ABCD customer')
This will create a table that looks like this:
Id Company CustomerName
------------------------------------
1 MSFT First MSFT customer
2 MSFT Second MSFT customer
3 ABCD First ABCD customer
4 MSFT Third MSFT customer
5 ABCD Second ABCD customer
Now run the following query on it:
select
Company + right('00000' + cast(ROW_NUMBER() over (partition by Company order by Id) as varchar(5)),5) as SpecialKey,
*
from
customers
This returns the same table, but with an additional column with your "special key":
SpecialKey Id Company CustomerName
---------------------------------------------
ABCD00001 3 ABCD First ABCD customer
ABCD00002 5 ABCD Second ABCD customer
MSFT00001 1 MSFT First MSFT customer
MSFT00002 2 MSFT Second MSFT customer
MSFT00003 4 MSFT Third MSFT customer
You could create a view with this query and let everyone use that view, to make sure everyone sees the "special key" column.
However, this solution has two downsides:
You need at least SQL Server 2005 in
order for row_number to work.
The numbers in the special key will change when you delete companies from the table. So, if you don't want the numbers to change, you have to make sure that nothing is ever deleted from that table.

Find the last value in a "rolled-over" sequence with a stored procedure?

Suppose I had a set of alpha-character identifiers of a set length, e.g. always five letters, and they are assigned in such a way that they are always incremented sequentially (GGGGZ --> GGGHA, etc.). Now, if I get to ZZZZZ, since the length is fixed, I must "roll over" to AAAAA. I might have a contiguous block from ZZZAA through AAAAM. I want to write a sproc that will give me the "next" identifier, in this case AAAAN.
If I didn't have this "rolling over" issue, of course, I'd just ORDER BY DESC and grab the top result. But I'm at a bit of a loss now -- and it doesn't help at all that SQL is not my strongest language.
If I have to I can move this to my C# calling code, but a sproc would be a better fit.
ETA: I would like to avoid changing the schema (new column or new table); I'd rather just be able to "figure it out". I might even prefer to do it brute force (e.g. start at the lowest value and increment until I find a "hole"), even though that could get expensive. If you have an answer that does not modify the schema, it'd be a better solution for my needs.
Here's code that I think will give you your Next value. I created 3 functions. The table is just my simulation of the table.column with your alpha ids (I used MyTable.AlphaID). I assume that it's as you implied and there is one contiguous block of five-character uppercase alphabetic strings (AlphaID):
IF OBJECT_ID('dbo.MyTable','U') IS NOT NULL
DROP TABLE dbo.MyTable
GO
CREATE TABLE dbo.MyTable (AlphaID char(5) PRIMARY KEY)
GO
-- Play with different population scenarios for testing
INSERT dbo.MyTable VALUES ('ZZZZY')
INSERT dbo.MyTable VALUES ('ZZZZZ')
INSERT dbo.MyTable VALUES ('AAAAA')
INSERT dbo.MyTable VALUES ('AAAAB')
GO
IF OBJECT_ID('dbo.ConvertAlphaIDToInt','FN') IS NOT NULL
DROP FUNCTION dbo.ConvertAlphaIDToInt
GO
CREATE FUNCTION dbo.ConvertAlphaIDToInt (#AlphaID char(5))
RETURNS int
AS
BEGIN
RETURN 1+ ASCII(SUBSTRING(#AlphaID,5,1))-65
+ ((ASCII(SUBSTRING(#AlphaID,4,1))-65) * 26)
+ ((ASCII(SUBSTRING(#AlphaID,3,1))-65) * POWER(26,2))
+ ((ASCII(SUBSTRING(#AlphaID,2,1))-65) * POWER(26,3))
+ ((ASCII(SUBSTRING(#AlphaID,1,1))-65) * POWER(26,4))
END
GO
IF OBJECT_ID('dbo.ConvertIntToAlphaID','FN') IS NOT NULL
DROP FUNCTION dbo.ConvertIntToAlphaID
GO
CREATE FUNCTION dbo.ConvertIntToAlphaID (#ID int)
RETURNS char(5)
AS
BEGIN
RETURN CHAR((#ID-1) / POWER(26,4) + 65)
+ CHAR ((#ID-1) % POWER(26,4) / POWER(26,3) + 65)
+ CHAR ((#ID-1) % POWER(26,3) / POWER(26,2) + 65)
+ CHAR ((#ID-1) % POWER(26,2) / 26 + 65)
+ CHAR ((#ID-1) % 26 + 65)
END
GO
IF OBJECT_ID('dbo.GetNextAlphaID','FN') IS NOT NULL
DROP FUNCTION dbo.GetNextAlphaID
GO
CREATE FUNCTION dbo.GetNextAlphaID ()
RETURNS char(5)
AS
BEGIN
DECLARE #MaxID char(5), #ReturnVal char(5)
SELECT #MaxID = MAX(AlphaID) FROM dbo.MyTable
IF #MaxID < 'ZZZZZ'
RETURN dbo.ConvertIntToAlphaID(dbo.ConvertAlphaIDToInt(#MaxID)+1)
IF #MaxID IS NULL
RETURN 'AAAAA'
SELECT #MaxID = MAX(AlphaID)
FROM dbo.MyTable
WHERE AlphaID < dbo.ConvertIntToAlphaID((SELECT COUNT(*) FROM dbo.MyTable))
IF #MaxID IS NULL
RETURN 'AAAAA'
RETURN dbo.ConvertIntToAlphaID(dbo.ConvertAlphaIDToInt(#MaxID)+1)
END
GO
SELECT * FROM dbo.MyTable ORDER BY dbo.ConvertAlphaIDToInt(AlphaID)
GO
SELECT dbo.GetNextAlphaID () AS 'NextAlphaID'
By the way, if you don't want to assume contiguity, you can do as you suggested and (if there's a 'ZZZZZ' row) use the first gap in the sequence. Replace the last function with this:
IF OBJECT_ID('dbo.GetNextAlphaID_2','FN') IS NOT NULL
DROP FUNCTION dbo.GetNextAlphaID_2
GO
CREATE FUNCTION dbo.GetNextAlphaID_2 ()
RETURNS char(5)
AS
BEGIN
DECLARE #MaxID char(5), #ReturnVal char(5)
SELECT #MaxID = MAX(AlphaID) FROM dbo.MyTable
IF #MaxID < 'ZZZZZ'
RETURN dbo.ConvertIntToAlphaID(dbo.ConvertAlphaIDToInt(#MaxID)+1)
IF #MaxID IS NULL
RETURN 'AAAAA'
SELECT TOP 1 #MaxID=M1.AlphaID
FROM dbo.Mytable M1
WHERE NOT EXISTS (SELECT 1 FROM dbo.MyTable M2
WHERE AlphaID = dbo.ConvertIntToAlphaID(dbo.ConvertAlphaIDToInt(M1.AlphaID) + 1 )
)
ORDER BY M1.AlphaID
IF #MaxID IS NULL
RETURN 'AAAAA'
RETURN dbo.ConvertIntToAlphaID(dbo.ConvertAlphaIDToInt(#MaxID)+1)
END
GO
You'd have to store the last allocated identifier in the sequence.
For example, store it in another table that has one column & one row.
CREATE TABLE CurrentMaxId (
Id CHAR(6) NOT NULL
);
INSERT INTO CurrentMaxId (Id) VALUES ('AAAAAA');
Each time you allocate a new identifier, you'd fetch the value in that tiny table, increment it, and store that value in your main table as well as updating the value in CurrentMaxId.
The usual caveats apply with respect to concurrency, table-locking, etc.
I think I'd have tried to store the sequence as an integer, then translate it to string. Or else store a parallel integer column that is incremented at the same time as the alpha value. Either way, you could sort on the integer column.
A problem here is that you can't really tell from the data where the "last" entry is unless there is more detail as to how the old entries are deleted.
If I understand correctly, you are wrapping around at the end of the sequence, which means you must be deleting some of your old data to make space. However if the data isn't deleted in a perfectly uniform manner, you'll end up with fragments, like below:
ABCD HIJKL NOPQRS WXYZ
You'll notice that there is no obvious next value...D could be the last value created, but it might also be L or S.
At best you could look for the first or last missing element (use a stored procedure to perform a x+1 check just like you would to find a missing element in an integer sequence), but it's not going to provide any special result for rolled-over lists.
Since I don't feel like writing code to increment letters, I'd create a table of all valid IDs (AAAAAA through ZZZZZZ) with an integer from 1 to X for those IDs. Then you can use the following:
SELECT #max_id = MAX(id) FROM Possible_Silly_IDs
SELECT
COALESCE(MAX(PSI2.silly_id), 'AAAAAA')
FROM
My_Table T1
INNER JOIN Possible_Silly_IDs PSI1 ON
PSI1.silly_id = T1.silly_id
INNER JOIN Possible_Silly_IDs PSI2 ON
PSI2.id = CASE WHEN PSI1.id = #max_id THEN 1 ELSE PSI1.id + 1 END
LEFT OUTER JOIN My_Table T2 ON
T2.silly_id = PSI2.silly_id
WHERE
T2.silly_id IS NULL
The COALESCE is there in case the table is empty. To be truly robust you should calculate the 'AAAAAA' (SELECT #min_silly_id = silly_id WHERE id = 1) in case your "numbering" algorithm changes.
If you really wanted to do things right, you'd redo the database design as has been suggested.
I think the lowest-impact solution for my needs is to add an identity column. The one thing I can guarantee is that the ordering will be such that entries that should "come first" will be added first -- I'll never add one with identifier BBBB, then go back and add BBBA later. If I didn't have that constraint, obviously it wouldn't work, but as it stands, I can just order by the identity column and get the sort I want.
I'll keep thinking about the other suggestions -- maybe if they "click" in my head, they'll look like a better option.
To return the next ID for a given ID (with rollover), use:
SELECT COALESCE
(
(
SELECT TOP 1 id
FROM mytable
WHERE id > #id
ORDER BY
id
),
(
SELECT TOP 1 id
FROM mytable
ORDER BY
id
)
) AS nextid
This query searches for the ID next to the given. If there is no such ID, it returns the first ID.
Here are the results:
WITH mytable AS
(
SELECT 'AAA' AS id
UNION ALL
SELECT 'BBB' AS id
UNION ALL
SELECT 'CCC' AS id
UNION ALL
SELECT 'DDD' AS id
UNION ALL
SELECT 'EEE' AS id
)
SELECT mo.id,
COALESCE
(
(
SELECT TOP 1 id
FROM mytable mi
WHERE mi.id > mo.id
ORDER BY
id
),
(
SELECT TOP 1 id
FROM mytable mi
ORDER BY
id
)
) AS nextid
FROM mytable mo
id nextid
----- ------
AAA BBB
BBB CCC
CCC DDD
DDD EEE
EEE AAA
, i. e. it returns BBB for AAA, CCC for BBB, etc., and, finally, AAA for EEE which is last in the table.