How to create conditional unique constraint - sql

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.

Related

Is there a way to insert the same row values in a temp table? SQL Server

I have a temp table #t that has a column already called ID with about 75 values. I've inserted another column called status and I want all of the values in the 'status' column to equal "A". Is there a way I can do this without having to manually insert A for each row?
Would want it to look like this but for all 75 rows
|ID| |Status|
----------------
|24| | A |
Not sure if I understand your question correctly but you could do it with something like
update #t set Status = 'A'
You can do that by setting the default value for status column.
Create table #test(id int, status varchar(1) default 'A');
insert into #test (id) values (1),(2),(3);
Select * From #test;
id status
1 A
2 A
3 A
If your table is already created you may set the default value as the following:
ALTER TABLE #test ADD CONSTRAINT df_val DEFAULT 'A' FOR status;
See a demo from db<>fiddle.

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

How to populate a column's value based on two other column's values

I have a table like this:
| DEL_ID | CP_ID | ID | QUANTITY | FP_ID | RESULT |
I need to populate the result column by using the Quantity and FP_ID columns by concatenating the FP_ID value to itself, and do so as many times as the value of QUANTITY.
So RESULT = FP_ID concatenated to itself QUANTITY times.
If QUANTITY is 3 and FP_ID is 23 then the result should be 232323
I need to do an insert that inserts this result for each row based on this logic.
How do I do this in SQL?
It should be computed column....
I mean I should be declared as it is based on other two columns
if you want to add after creation of table
ALTER TABLE tblResults
ADD Final_Result as replicate(FP_ID ,Quantity)
else
while creation
Create table tblResults
(.......ur columns..... , Final_Result as replicate(FP_ID ,Quantity))
you no need to give data for this column, It will automatically loaded when the data is loaded into table "tblResults"
NOTE: If any value is null then the Final_Result value will also be NULL
I have edited my answer as below:
Please execute the below query to create a Function:
CREATE FUNCTION [dbo].[ConcatenateString]
(
#FP_ID INT,
#QUANTITY INT
)
RETURNS
NVARCHAR(MAX)
AS
BEGIN
DECLARE #ConcatenatedString NVARCHAR(MAX), #ConvertedFP_ID NVARCHAR(100)
SET #ConcatenatedString =''
SET #ConvertedFP_ID = CONVERT(varchar(100),#FP_ID)
WHILE #QUANTITY >= 1
BEGIN
SELECT #ConcatenatedString = #ConcatenatedString+#ConvertedFP_ID
SET #QUANTITY = #QUANTITY - 1
END
RETURN #ConcatenatedString
END
GO
And you can call the Function in the INSERT script:
INSERT INTO tblResults(DEL_ID,CP_ID,ID,QUANTITY,FP_ID,RESULT)
VALUES(1,2,3,4,5,(SELECT dbo.ConcatenateString(4,5) AS ConcatnatedValue))

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.