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

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.

Related

Insert new record into autonumbered table, and then use the autonumber in another table

I'm writing a stored procedure to insert data from a form into two tables. One table has an autonumbered identity field. I need to insert the data into that table, find the newly created autonumber, and use that number to insert data into another table. So, to boil it down, I have a one-to-many link between the two tables and I need to make sure the identity field gets inserted.
Is this code the best way to do something like this, or am I missing something obvious?
CREATE PROCEDURE [dbo].[sp_Insert_CRT]
(
#TRACKING_ID int,
#CUST_NUM int,
#TRACKING_ITEM_ID int,
#STATEMENT_NUM nvarchar (200) = null,
#AMOUNT numeric (15, 2),
#BBL_ADJUSTED int = NULL,
#PAID_VS_BILLED int = NULL,
#ADJUSTMENT_TYPE int = NULL,
#ENTERED_BY nvarchar (10) = NULL,
#ENTERED_DATE date = NULL,
#AA_STATUS int = NULL
)
AS
BEGIN
-- Insert data into CRT_Main, where Tracking_ID is an autonumber field
INSERT into tbl_CRT_Main
(
-- TRACKING_ID
CUST_NUM
,TRACKING_ITEM_ID
,STATEMENT_NUM
,AMOUNT
)
VALUES
(
-- #TRACKING_ID
#CUST_NUM
,#TRACKING_ITEM_ID
,#STATEMENT_NUM
,#AMOUNT
)
-- Find the newly generated autonumber, and use it in another table
BEGIN TRANSACTION
DECLARE #TrackID int;
SELECT #TrackID = coalesce((select max(TRACKING_ID) from tbl_CRT_Main), 1)
COMMIT
INSERT into tbl_CRT_Admin_Adjustment
(
TRACKING_ID
,BBL_ADJUSTED
,PAID_VS_BILLED
,[ADJUSTMENT_TYPE]
,[ENTERED_BY]
,[ENTERED_DATE]
,AA_STATUS
)
VALUES
(
#TrackID
,#BBL_ADJUSTED
,#PAID_VS_BILLED
,#ADJUSTMENT_TYPE
,#ENTERED_BY
,#ENTERED_DATE
,#AA_STATUS
)
END
SELECT #TrackID = coalesce((select max(TRACKING_ID) from tbl_CRT_Main), 1)
No, don't do this. This will get you the maximum value of TRACKING_ID yes, but that doesn't mean that's the value that was created for your INSERT. If multiple INSERT statements were being run by different connections then very likely you would get the wrong value.
Instead, use SCOPE_IDENTITY to get the value:
SET #TrackID = SCOPE_IDENTITY();
Also, there is no need to wrap the above in an explicit transaction like you have with your SELECT MAX(). Instead, most likely, the entire batch in the procedure should be inside it's own explicit transaction, with a TRY...CATCH so that you can ROLLBACK the whole batch in the event of an error.

Generating a unique batch id (SQL Server)

This is possible 2x questions in 1x. Sorry about that, but here goes:
PROBLEM
I am creating a unique batch id everytime a user uploads some data to SQL Server. Currently, I do this by looking at the last value of the 'Identity Specification' and add +1 to that.
Problem arises, as you might have guessed, if multiple users input data at the same, they both would get the same batch id...
Possible Solution
In order to mitigate this issue, I have come up with this method to generate 3 letter + random number; and the (last id value + 1):
DECLARE #tmp CHAR(3) = CHAR(CAST(RAND()*26 AS int)+65) + CHAR(CAST(RAND()*26 AS int)+65) + CHAR(CAST(RAND()*26 AS int)+65);
SELECT #tmp;
select cast(RAND()*9999 as int)
(1) I am not sure how to concatenate this into one line of string.
(2) The other question, is there a way to 100% guarantee every user is given a unique batch id every time they submit a request, regardless of how many are doing it simultaneously?
I would really appreciate your input in this.
1 - Concatenation part is very simple, you can do the following:
DECLARE #tmp VARCHAR(10);
SET #tmp = CHAR(CAST(RAND()*26 AS int)+65)
+ CHAR(CAST(RAND()*26 AS int)+65)
+ CHAR(CAST(RAND()*26 AS int)+65)
+ CAST(cast(RAND()*9999 as int) AS VARCHAR(4));
SELECT #tmp;
2 - I would suggest to populate a table with the Random values you would like to issue to users and then select from it, to avoid the race-condition.
Create a table called BatchNumbers with two Columns BatchNumber and Used.
Populate the batch number table and 0 as default value for Used Column.
Then everytime you need a batch number do the following.
CREATE PROC dbo.usp_Get_BatchNumber
#BatchNumber VARCHAR(10) OUTPUT
AS
BEGIN
SET NOCOUNT ON;
Declare #t TABLE (BN VARCHAR(10));
UPDATE TOP (1) BatchNumbers
SET Used = 1
OUTPUT inserted.BatchNumber INTO #t (BN )
WHERE Used = 0;
SELECT #BatchNumber = BN FROM #t;
END
You need an "Upload" table with a Bigint Identity column for the BatchID, then add a new row for every user upload.
The server will maintain the correct values and prevent collisions.
I would use the built in function for this:
select newid()
> 240CA878-135E-4176-AE57-0FA83FF74037
For the first problem, you can either create a variable for your random number as a char(4) and just simply concatenate the 2, or create it as an int and then CAST it as a VARCHAR while concatenating. Everything that is concatenated into a string must be a string.
DECLARE #tmp CHAR(3) = CHAR(CAST(RAND()*26 AS int)+65) + CHAR(CAST(RAND()*26 AS int)+65) + CHAR(CAST(RAND()*26 AS int)+65);
SELECT #tmp;
DECLARE #randNum VARCHAR(4) = CAST(RAND()*9999 AS INT)
-- OR DECLARE #randNum INT = CAST(Rand()*9999) AS INT)
SELECT #randNum
DECLARE #batchID VARCHAR(MAX) = #tmp + #randNum
-- OR DECLARE #batchID VARCHAR(MAX) = #tmp + CAST(#randNum AS VARCHAR)
SELECT #batchID
try the following:
1)
DECLARE #tmp CHAR(7) = CHAR(CAST(RAND()*26 AS int)+65) + CHAR(CAST(RAND()*26 AS int)+65) + CHAR(CAST(RAND()*26 AS int)+65) + cast(cast(RAND()*9999 as int) as varchar(4));
SELECT #tmp;
2) Yes, I think so.
I upvoted Terry Carmen's answer, but from his comments it sounds like he's suggesting something different from what I first thought, so here's a complete example. I think you want a table that has a key defined with the IDENTITY property, which will tell SQL Server that you want unique, sequential values in that column and you want the database to worry about the details of guaranteeing that this is so.
create table dbo.Import
(
-- identity(1, 1) means that SQL Server will automatically assign values for
-- this column when you insert a record, with 1 being the first value
-- assigned and each subsequent value incrementing by 1.
Identifier bigint not null identity(1, 1),
-- This column for illustration only; replace it with whatever data you need
-- to store.
YourStuffHere varchar(max)
);
-- Now simply use any INSERT or MERGE command against dbo.Import, and omit the
-- Identifier column from the list of columns whose values the command supplies.
-- Then you can use the SCOPE_IDENTITY() function or an OUTPUT clause to capture
-- the Identifier value that SQL Server has inserted.
-- Example 1: INSERT with explicit values and OUTPUT.
insert dbo.Import
(YourStuffHere)
output
inserted.Identifier
values
('Example 1');
-- Example 2: INSERT/SELECT with OUTPUT.
insert dbo.Import
(YourStuffHere)
output
inserted.Identifier
select
'Example 2';
-- Example 3: INSERT with SCOPE_IDENTITY().
insert dbo.Import
(YourStuffHere)
values
('Example 3');
select Identifier = convert(bigint, scope_identity());
-- Show table contents.
select * from dbo.Import;
The first INSERT statement above produces the following result:
Identifier
1
The second:
Identifier
2
The SELECT following the third INPUT gives:
Identifier
3
And the final SELECT shows you the contents of the table:
Identifier YourStuffHere
1 Example 1
2 Example 2
3 Example 3
This is the easiest way to go about this as it allows SQL Server to do all the real work for you. Please let me know if I've misunderstood your requirements.

Trouble updating log with triggers using SQL Server

I am trying to create a trigger with a higher difficulty that would let me create a log after updating rows in alumns table
| Alumn_ID | Name | Courses | Favourite_Course
1 Peter 5 Math
And this would be the result if for example someone updated the number of courses from 5 to 6.
| Log_ID | Alumn_ID | Note | NoteID | Change_Date | Last_Change_Date
1 1 Fields were Updated Note 1 2018-04-23 00:00:00.000 2018-03-23 00:00:00.000
Here is my current trigger
ALTER TRIGGER [LOG]
ON ALUMNS
AFTER UPDATE
AS
BEGIN
DECLARE #Note VARCHAR(50)
DECLARE #Alumn_ID varchar;
SELECT #Alumn_ID= INSERTED.Alumn_ID FROM INSERTED
SET #Note = 'Fields were updated'
INSERT INTO Alumn_Log (Log_ID, Alumn_ID, Note, NoteID, Change_Date)
SELECT Log_ID, i.Alumn_ID, #Note, NoteID, GETDATE(); FROM INSERTED i
END
My problem is:
How do i create the Log ID and the Note ID that i can't take from INSERTED i?
My second problem is, how do i insert the current date? when i try to execute the query it tells me that i can't use that variable in INSERTS.
My third problem, is how can i put the "Last change date"?
Fourth, is there a way to type an specific Note for example if only the name was changed it should say "Name was changed"?
Finally, The Note ID would be Varchar not identity and every note ID needs to be different
This is the current and only error that's preventing me from running the Query:
This is what i get Msg 273, level 16, state 1, procedure Log_Trigger, line 19 [Batch Start Line 0] me time stamp Use INSERT with a list of columns to exclude the timestamp column or insert DEFAULT in the timestamp column.
Here is how I would approach it.
How do i create the Log ID and the Note ID that i can't take from
INSERTED i?
The Log Id can be an AutoIdentity column. An INT column with IDENTITY INSERT.
The Note ID can be an Auto Incremented Computed column (shown in the code below). You would probably need to introduce a new column that serves as a prefix.
My second problem is, how do i insert the current date? when i try to
execute the query it tells me that i can't use that variable in
INSERTS.
GETDATE()?
My third problem, is how can i put the "Last change date"?
You can have a join with INSERTED and get the value from the log from a previous row. Shown in the code.
Fourth, is there a way to type an specific Note for example if only
the name was changed it should say "Name was changed"?
That would depend on finding the nature of the update on which column. This is more of a business question than a technical question.
Finally, The Note ID would be Varchar not identity and every note ID needs to be different
Now, the code (the entire schema)
CREATE TABLE LOG(
Log_ID INT IDENTITY(1, 1) NOT NULL,
Alumn_ID INT,
NOTE VARCHAR(200),
PREFIX VARCHAR(30),
NOTEID AS([PREFIX] + RIGHT('0000000' + CAST(Log_ID AS VARCHAR(7)), 7)) PERSISTED,
CHANGEDATE DATETIME,
LASTCHANGEDATE DATETIME
);
CREATE TABLE ALUMN(
Alumn_ID INT,
NAME VARCHAR(50),
COURSES INT,
FAVORITE_COURSE VARCHAR(50)
);
CREATE TRIGGER[trg_LOG]
ON ALUMN
AFTER UPDATE
AS
BEGIN
DECLARE #Note VARCHAR(50)
--DECLARE #Alumn_ID VARCHAR(50)
DECLARE #Lastchange DATETIME
--SELECT #Alumn_ID = INSERTED.Alumn_ID FROM INSERTED
SET #Note = 'Fields were updated'
SELECT #Lastchange = CHANGEDATE FROM LOG l
INNER JOIN INSERTED i ON l.Alumn_ID = i.Alumn_ID
--INNER JOIN ALUMN
INSERT INTO LOG(Alumn_ID, Note, Prefix, CHANGEDATE, LASTCHANGEDATE)
SELECT i.Alumn_ID, #Note, 'AUP', GETDATE(), #Lastchange FROM INSERTED i
END
how do i insert the current date? when i try to execute the query it
tells me that i can't use that variable in INSERTS.
SELECT Log_ID, i.Alumn_ID, #Note, NoteID, GETDATE(); FROM INSERTED i
Take the semi-colon out of the line above.
SELECT Log_ID, i.Alumn_ID, #Note, NoteID, GETDATE() FROM INSERTED i

Optimizing SQL query to return Record with tags

I was looking for help to optimize a query I am writing for SQL Server. Given this database schema:
TradeLead object, a record in this table is a small article.
CREATE TABLE [dbo].[TradeLeads]
(
[TradeLeadID] INT NOT NULL PRIMARY KEY IDENTITY(1,1),
Title nvarchar(250),
Body nvarchar(max),
CreateDate datetime,
EditDate datetime,
CreateUser nvarchar(250),
EditUser nvarchar(250),
[Views] INT NOT NULL DEFAULT(0)
)
Here's the cross reference table to link a TradeLead article to an Industry record.
CREATE TABLE [dbo].[TradeLeads_Industries]
(
[ID] INT NOT NULL PRIMARY KEY IDENTITY(1,1),
[TradeLeadID] INT NOT NULL,
[IndustryID] INT NOT NULL
)
Finally, the schema for the Industry object. These are essentially just tags, but a user is unable to enter these. The database will have a specific amount.
CREATE TABLE [dbo].[Industries]
(
IndustryID INT NOT NULL PRIMARY KEY identity(1,1),
Name nvarchar(200)
)
The procedure I'm writing is used to search for specific TradeLead records. The user would be able to search for keywords in the title of the TradeLead object, search using a date range, and search for a TradeLead with specific Industry Tags.
The database will most likely be holding around 1,000,000 TradeLead articles and about 30 industry tags.
This is the query I have come up with:
DECLARE #Title nvarchar(50);
SET #Title = 'Testing';
-- User defined table type containing a list of IndustryIDs. Would prob have around 5 selections max.
DECLARE #Selectedindustryids IndustryIdentifierTable_UDT;
DECLARE #Start DATETIME;
SET #Start = NULL;
DECLARE #End DATETIME;
SET #End = NULL;
SELECT *
FROM(
-- Subquery to return all the tradeleads that match a user's criteria.
-- These fields can be null.
SELECT TradeLeadID,
Title,
Body,
CreateDate,
CreateUser,
Views
FROM TradeLeads
WHERE(#Title IS NULL OR Title LIKE '%' + #Title + '%') AND (#Start IS NULL OR CreateDate >= #Start) AND (#End IS NULL OR CreateDate <= #End)) AS FTL
INNER JOIN
-- Subquery to return the TradeLeadID for each TradeLead record with related IndustryIDs
(SELECT TI.TradeLeadID
FROM TradeLeads_Industries TI
-- Left join the selected IndustryIDs to the Cross reference table to get the TradeLeadIDs that are associated with a specific industry.
LEFT JOIN #SelectedindustryIDs SIDS
ON SIDS.IndustryID = TI.IndustryID
-- It's possible the user has not selected any IndustryIDs to search for.
WHERE (NOT EXISTS(SELECT 1 FROM #SelectedIndustryIDs) OR SIDS.IndustryID IS NOT NULL)
-- Group by to reduce the amount of records.
GROUP BY TI.TradeLeadID) AS SelectedIndustries ON SelectedIndustries.TradeLeadID = FTL.TradeLeadID
With about 600,000 TradeLead records and with an average of 4 IndustryIDs attached to each one, the query takes around 8 seconds to finish on a local machine. I would like to get it as fast as possible. Any tips or insight would be appreciated.
There's a few points here.
Using constructs like (#Start IS NULL OR CreateDate >= #Start) can cause a problem called parameter sniffing. Two ways of working around it are
Add Option (Recompile) to the end of the query
Use dynamic SQL to only include the criteria that the user has asked for.
I would favour the second method for this data.
Next, the query can be rewritten to be more efficient by using exists (assuming the user has entered industry ids)
select
TradeLeadID,
Title,
Body,
CreateDate,
CreateUser,
[Views]
from
dbo.TradeLeads t
where
Title LIKE '%' + #Title + '%' and
CreateDate >= #Start and
CreateDate <= #End and
exists (
select
'x'
from
dbo.TradeLeads_Industries ti
inner join
#Selectedindustryids sids
on ti.IndustryID = sids.IndustryID
where
t.TradeLeadID = ti.TradeLeadID
);
Finally you will want at least one index on the dbo.TradeLeads_Industries table. The following are candidates.
(TradeLeadID, IndustryID)
(IndustryID, TradeLeadID)
Testing will tell you whether one or both is useful.

Incrementing custom primary key values in 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