Generating name based on ID and current year using SQL Server - sql

I am trying to generate value for the ProjektNummer (Name) column based on the value of the ID (primary key column.
So for example, when ID is 142, then the ProjektNummer should be 19142. 19 indicating the current year and 142 is the value of its ID.
Now, when the year changes, the id part in the value of the project name column needs to be restarted from zero while the actual ID should follow the identity and be incremented by 1 as usual.
So if the last record in the Year 2019 has ID=164, the first record in the year 2020 should be:
ID: 165 ProjektNummer: 20001
.. and the second record in year 2020 would be:
ID: 166 ProjektNummer: 20002
One way to achieve this is by creating a new view say 'vMaxLastId' and storing the max value of 'ID' for its corresponding year. This record will be used as a reference in another trigger.
So, if the first record in the year 2020 has ID: 165 Another trigger will subtract the value (164) for year 2019 (which is stored in the view), from the value of 'ID' after inserting a new record in 'table1' using the second trigger. (this should be done before inserting tho)
165-164=1
166-164=2
167-164=3
....
When the year will change from 2019 to 2020 the max id and year in 2020 will be added as a new record in the view. I have implemented this but there are multiple problems associated with this approach.
Kindly suggest me any possible way to solve this problem. I am willing to make any kind of change to the DB, query or trigger or the whole concept.

The first thought that crossed my mind would be to create a SEQUENCE object to generate the ID portion of the value. Since there are no fancy rules around the numbering, just this should do.
CREATE SEQUENCE dbo.ProjektID;
That will give you a bigint. To consume it, CONCAT that to the current year, and put your padding zeros onto the sequence value. You could set this as the default value for the column.
CONCAT( YEAR( GETDATE() ) % 100 , RIGHT( CONCAT('000', NEXT VALUE FOR dbo.ProjektID), 3))
Then schedule a job to run at midnight on the first of the year to restart the sequence.
ALTER SEQUENCE dbo.ProjektID
RESTART;
I think that would cover your requirements.

The most pragmatic solution I could think of is the use of a INSTEAD OF trigger on that specific main table.
My test table:
CREATE TABLE dbo.MyTestProject
(
id INT IDENTITY(1, 1) NOT NULL PRIMARY KEY
, dateInserted DATETIME2(7) NOT NULL
, projectNumber INT NULL
);
And the example trigger:
CREATE TRIGGER dbo.triMyTestProject
ON dbo.MyTestProject
INSTEAD OF INSERT
AS
BEGIN
SET NOCOUNT ON;
DECLARE
#maxYear INT
, #maxNumber INT
, #startNumber INT
, #dateInserted DATETIME2;
SELECT
#dateInserted = dateInserted
FROM Inserted;
SET #startNumber = CAST(FORMAT(#dateInserted, 'yy') AS INT) * 1000 + 1;
SELECT
#maxNumber = MAX(projectNumber)
, #maxYear = ISNULL(MAX(YEAR(dateInserted)), YEAR(#dateInserted))
FROM dbo.MyTestProject;
INSERT INTO dbo.MyTestProject
(
dateInserted
, projectNumber
)
VALUES
(
#dateInserted, IIF(YEAR(#dateInserted) > #maxYear OR #maxNumber IS NULL, #startNumber, #maxNumber + 1)
);
END;
Add test data:
INSERT INTO dbo.MyTestProject (dateInserted) VALUES (SYSDATETIME());
INSERT INTO dbo.MyTestProject (dateInserted) VALUES (SYSDATETIME());
INSERT INTO dbo.MyTestProject (dateInserted) VALUES (SYSDATETIME());
INSERT INTO dbo.MyTestProject (dateInserted) VALUES ('20200102');
INSERT INTO dbo.MyTestProject (dateInserted) VALUES ('20200202');
Test result:
SELECT * FROM dbo.MyTestProject;
id dateInserted projectNumber
----------- --------------------------- -------------
1 2019-12-06 17:27:58.8813628 19001
2 2019-12-06 17:27:58.8833638 19002
3 2019-12-06 17:27:58.8883448 19003
4 2020-01-02 00:00:00.0000000 20001
5 2020-02-02 00:00:00.0000000 20002

Related

SQL matching with rest of the columns if any of the where condition parameter is missing

Here is my table structure:
ID cid Name Course Interval
1 1 KB Y 2
2 1 TB Y 3
3 2 BK N 1
I need to write a query which returns all rows with matching condition.
if at all any of the parameter is null or not provided then i need to return all of the matching rows.
In my select query if my parameters are (cid ==1 and Name== null and course ==Y or (cid ==1 and course ==Y ) then I need to return rows with id
1 and 2.
What exactly I need is this:
If I get all the matching record then i can take interval corresponding to the record. Else I need to take average interval of the matching record
Is this what you are after? Its a variable number or arguments - in my case the parameters are explicitly defined, yours may be passed in from a stored proc etc.
-- Create the table
create table #t(ID int, cid int, Name char(2), Course char(1), Interval int)
insert #t values (1,1,'KB','Y',2)
,(2,1,'TB','Y',3)
,(3,2,'BK','N',1)
-- Declare the arguments
declare #cid int
declare #name char(2)
declare #course char(1)
-- Set one or more arguments
set #cid=1
set #name=null
set #course='Y'
select AVG(convert(decimal(5,2),interval)) from #t
where isnull(#cid,cid)=cid
and isnull(#name,name)=name
and isnull(#course,course)=course

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

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

How to create conditional unique constraint

Having a table:Table1 in which a column Code accepts nullables values how can we insure that values are unique for non nullable values except for codes that start with 'A' which can be duplicated maximum twice?
Table1
Id | Code
----------
1 | NULL --[ok]
2 | A123 --[ok]
3 | A123 --[ok]
4 | B100 --[ok]
5 | C200 --[ok]
6 | B100 --[not ok already used]
7 | NULL --[ok]
What i have tried is creating an indexed view, the solution work fine for NULL values but not for the second case i mentioned (skipped actualy)
Create view v_Table_unq with schemabinding as(
select code from
dbo.Table1
where code is not null and code not like 'A%'
)
go
create unique clustered index unq_code on v_Table_unq(code)
Thanks for help
Table Creation
CREATE TABLE CheckConstraint
(
Name VARCHAR(50),
)
GO
Function Creation
create FUNCTION CheckDuplicateWithA() RETURNS INT AS BEGIN
DECLARE #ret INT =0 ;
SELECT #ret = IsNull(COUNT(Name), 0) FROM CheckConstraint WHERE Name like '[A]%' group by Name having COUNT(name) >= 1;
RETURN IsNUll(#ret, 0);
END;
GO
create FUNCTION CheckDuplicateOtherThenA() RETURNS INT AS BEGIN
DECLARE #ret INT =0 ;
SELECT #ret = IsNull(COUNT(Name), 0) FROM CheckConstraint WHERE Name not like '[A]%' group by Name having COUNT(name) >= 1;
RETURN IsNUll(#ret, 0);
END;
GO
Constraints
alter TABLE CheckConstraint
add CONSTRAINT CheckDuplicateContraintWithA CHECK (NOT (dbo.CheckDuplicateWithA() > 2));
go
alter TABLE CheckConstraint
add CONSTRAINT CheckDuplicateConmstraintOtherThenA CHECK (NOT (dbo.CheckDuplicateOtherThenA() > 1));
go
Result Set
insert into CheckConstraint(Name)Values('b') -- Passed
insert into CheckConstraint(Name)Values('b') -- Failed
insert into CheckConstraint(Name)Values('a') -- Passed
insert into CheckConstraint(Name)Values('a') -- Passed
insert into CheckConstraint(Name)Values('a') -- Failed
Why would you want a unique contraint? Why cant add this logic in the proc which inserts the data in the table?If you do not have a single point of insertion/updation etc?Why cant put it in instead of or after trigger?That would be much better as you can handle it well and could return proper errror messages.This will have less overhead than having a index view which will add to overhead.If you need unique constraint for the records which doesnt start with 'A' then you can have a persisted column and have a unique constraint on that.
Off course you will have overhead of having persisted computed column with index..But if you just need unique contsraint you can use that.For values which starts with 'A' this could be a null value.

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.