SQL - Unique key across 2 columns of same table? - sql

I use SQL Server 2016. I have a database table called "Member".
In that table, I have these 3 columns (for the purpose of my question):
idMember [INT - Identity - Primary Key]
memEmail
memEmailPartner
I want to prevent a row to use an email that already exists in the table.
Both email columns are not mandatory, so they can be left blank (NULL).
If I create a new Member:
If not blank, the values entered for "memEmail" and "memEmailPartner" (independently) should not be found in any other rows in columns memEmail nor memEmailPartner.
So if I want to create a row with email (dominic#email.com) I must not find any occurrences of that value in memEmail or memEmailPartner.
If I update an existing Member:
I must not find any occurrences of that value in memEmail or memEmailPartner, with the exception that I am updating the row (idMembre) which already have the value in memEmail or memEmailPartner.
--
From what I read on Google, it should be possible to do something with a Function-Based Check Constraint but I can't make that work.
Anyone have a solution to my problem ?
Thank you.

I may have misunderstood exactly what you were asking but it looks like you want a simple upsert query with IF EXISTS conditions.
DECLARE #emailAddress VARCHAR(255)= 'dominic#email.com', --dummy value
#id INT= 2; --dummy value
IF NOT EXISTS
(
SELECT 1
FROM #Member
WHERE memEmail = #emailAddress
OR memEmailPartner = #emailAddress
)
BEGIN
SELECT 'insert';
END;
ELSE IF EXISTS
(
SELECT 1
FROM #Member
WHERE idMember = #id
)
BEGIN
SELECT 'update';
END;

A trigger is the traditional way of doing doing what you're asking for. Here's a simple demo;
--if object_id('member') is not null drop table member
go
create table member (
idMember INT Identity Primary Key,
memEmail varchar(100),
memEmailPartner varchar(100)
)
go
create trigger trg_member on member after insert, update as
begin
set nocount on
if exists (select 1 from member m join inserted i on i.memEmail = m.memEmail and i.idMember <> m.idMember) or
exists (select 1 from member m join inserted i on i.memEmail = m.memEmailPartner and i.idMember <> m.idMember) or
exists (select 1 from member m join inserted i on i.memEmailPartner = m.memEmail and i.idMember <> m.idMember) or
exists (select 1 from member m join inserted i on i.memEmailPartner = m.memEmailPartner and i.idMember <> m.idMember)
begin
raiserror('Email addresses must be unique.', 16, 1)
rollback
end
end
go
insert member(memEmail, memEmailPartner) values('a#a.com', null), ('b#b.com', null), (null, 'c#c.com'), (null, 'd#d.com')
go
select * from member
insert member(memEmail, memEmailPartner) values('a#a.com', null) -- should fail
go
insert member(memEmail, memEmailPartner) values(null, 'a#a.com') -- should fail
go
insert member(memEmail, memEmailPartner) values('c#c.com', null) -- should fail
go
insert member(memEmail, memEmailPartner) values(null, 'c#c.com') -- should fail
go
insert member(memEmail, memEmailPartner) values('e#e.com', null) -- should work
go
insert member(memEmail, memEmailPartner) values(null, 'f#f.com') -- should work
go
select * from member
-- Make sure updates still work!
update member set memEmail = memEmail, memEmailPartner = memEmailPartner
I've not tested this extensively but it should be enough to get you started if you want to try this approach.
StuartLC notes the potential for the UDF check constraint to fail in set based updates and/or various other conditions, triggers don't have this problem.
Stuart also suggests reconsidering whether this should really be a database constraint or managed through business logic elsewhere. I'm inclined to agree - my gut feel here is that sooner or later you will come across a situation that requires email addresses to be reused, or in some other way not strictly unique.

TL;DR
The wisdom of applying this kind of business rule logic in the database needs to be reconsidered - this check is likely a better candidate for your application, or a stored procedure which acts as an insert gate keeper instead of direct new row inserts into the table.
Ignoring the Warnings
That said, I do believe that what you want is however possible in a constraint UDF, albeit with potentially atrocious performance consequences*1, and likely prone to race conditions in set based updates
Here's a user defined function which applies the unique email logic across both columns. Note that by the time the constraint is checked, that the row is IN the table already, hence the new row itself needs to be excluded from the duplicate checks.
My code also is depedent on ANSI NULL behaviour, i.e. that the predicates NULL = NULL and X IN (NULL) both return NULL, and hence are excluded from the failure check (in order to meet your requirement that NULLS do not fail the rule).
We also need to check for the insert of BOTH new columns being non-null, but duplicated.
So here's the a UDF doing the checking:
CREATE FUNCTION dbo.CheckUniqueEmails(#id int, #memEmail varchar(50),
#memEmailPartner varchar(50))
RETURNS bit
AS
BEGIN
DECLARE #retval bit;
IF #memEmail = #memEmailPartner
OR EXISTS (SELECT 1 FROM MyTable WHERE memEmail IS NOT NULL
AND memEmail IN(#memEmail, #memEmailPartner) AND idMember <> #id)
OR EXISTS (SELECT 1 FROM MyTable WHERE memEmailPartner IS NOT NULL
AND memEmailPartner IN(#memEmail, #memEmailPartner) AND idMember <> #id)
SET #retval = 0
ELSE
SET #retval = 1;
RETURN #retval;
END;
GO
Which is then enforced in a CHECK constraint:
ALTER TABLE MyTable ADD CHECK (dbo.CheckUniqueEmails(
idMember, memEmail, memEmailPartner) = 1);
I've put a SQLFiddle up here
Uncomment the 'failed' test cases to ensure that the above check constraint is working.
I haven't tested this with updates, and as per Martin's advice on the link, this will likely break on an insert with multiple rows.
*1 - we'll need indexes on BOTH email address columns.

Related

Prevent duplicate data in using After Insert Trigger

I can detect duplicate records, but when I'm inserting new data it will detect it as a duplicate record even if doesn't already exist.
Here is my code:
ALTER TRIGGER [dbo].[SDPRawInventory_Dup_Trigger]
ON [dbo].[SDPRawInventory]
AFTER INSERT
AS
DECLARE #Year float,
#Month float,
#SDPGroup nvarchar(255);
SELECT
#Year = i.InvYear, #Month = i.InvMonth, #SDPGroup = i.SDPGroup
FROM inserted i;
IF (SELECT COUNT(*) FROM SDPRawInventory A
WHERE A.InvYear = #Year
AND A.InvMonth = #Month
AND A.SDPGroup = #SDPGroup) >= 1
BEGIN
RAISERROR ('Duplicate data', 16, 1)
ROLLBACK;
END
ELSE
BEGIN
INSERT INTO SDPRawInventory
SELECT * FROM inserted;
END
This is the table
And to clarify there is no primary key nor unique identifier.
If you are unable to put a constraint in place, then you need to handle the fact that Inserted may have multiple records. And because its an after insert trigger, you don't need to do anything if no duplicates are found because the records are already inserted.
ALTER TRIGGER [dbo].[SDPRawInventory_Dup_Trigger]
ON [dbo].[SDPRawInventory]
AFTER INSERT
AS
BEGIN
SET NOCOUNT ON;
IF EXISTS (
SELECT 1
FROM dbo.SDPRawInventory S
INNER JOIN Inserted I ON
-- Test for a duplicate
S.InvYear = I.InvYear
AND S.InvMonth = I.InvMonth
AND S.SDPGroup = I.SDPGroup
-- But ensure the duplicate is a *different* record - assumes a unique ID
AND S.ID <> I.ID
)
BEGIN
THROW 51000, 'Duplicate data.', 1;
END;
END;
Note the simplified and modern error handling.
EDIT: And if you have no unique key, and no permission to add one, then you need an instead of trigger to only insert non-duplicates e.g.
ALTER TRIGGER [dbo].[SDPRawInventory_Dup_Trigger]
ON [dbo].[SDPRawInventory]
INSTEAD OF INSERT
AS
BEGIN
SET NOCOUNT ON;
-- Reject the entire insert if a single duplicate exists
-- Note if multiple records are inserted, some of which are duplicates and some of which aren't, they all get rejected
IF EXISTS (
SELECT 1
FROM dbo.SDPRawInventory S
INNER JOIN Inserted I ON
-- Test for a duplicate
A.InvYear = I.InvYear
AND A.InvMonth = I.InvMonth
AND A.SDPGroup = I.#SDPGroup
)
-- Test that Inserted itself doesn't contain duplicates
OR EXISTS (SELECT 1 FROM Inserted GROUP BY InvYear, InvMonth, SDPGroup HAVING COUNT(*) > 1)
BEGIN
THROW 51000, 'Duplicate data.', 1;
END;
INSERT INTO dbo.SDPRawInventory (SDP_SKU_DESC, WholeQty, InvYear, InvMonth, SDPGroup, invUOM, LooseQty)
SELECT SDP_SKU_DESC, WholeQty, InvYear, InvMonth, SDPGroup, invUOM, LooseQty
FROM Inserted I
WHERE NOT EXISTS (
SELECT 1
FROM dbo.SDPRawInventory S
-- Test for a duplicate
WHERE S.InvYear = I.InvYear
AND S.InvMonth = I.InvMonth
AND S.SDPGroup = I.SDPGroup
);
END;
Note: This doesn't do anything to handle existing duplicates.
This trigger is executed after the new records were inserted, so it will at least find the original records in the SELECT COUNT statement. Changing >= 1 into >= 2 can only partially fix this when inserting is guaranteed to occur one record as a time. Moreover, it will still fail when there were already multiple duplicated of the newly inserted record in the database before the insert.
You need to exclude the latest inserted records from the COUNT. But a better idea would probably be to add a UNIQUE constraint for preventing duplicates, so no trigger would be necessary.
If adding a constraint is not possible yet, you should initiate a clean-up process to eliminate the existing duplicates beforehand. Everything else is looks pretty flawed to me, since it is unlikely the current approach will ever bring the table into a state with no duplicates.
You are creating the infinite loop. You just have to remove the insertion part from your trigger.
ELSE
BEGIN
INSERT INTO SDPRawInventory
SELECT * FROM inserted;
END
This should not be in the trigger as trigger is called as part of insertion. you should not write actual insertion in to table in trigger.

Prevent circular reference in MS-SQL table

I have a Account table with ID and ParentAccountID. Here is the scripts to reproduce the steps.
If the ParentAccountID is NULL then that is considered as Top level account.
Every account should finally ends with top level account i.e ParentAccountID is NULL
Declare #Accounts table (ID INT, ParentAccountID INT )
INSERT INTO #Accounts values (1,NULL), (2,1), (3,2) ,(4,3), (5,4), (6,5)
select * from #Accounts
-- Request to update ParentAccountID to 6 for the ID 3
update #Accounts
set ParentAccountID = 6
where ID = 3
-- Now the above update will cause circular reference
select * from #Accounts
When request comes like to update ParentAccountID of an account, if that cause circular reference then before update its need to identified.
Any idea folks!!
It seems you've got some business rules defined for your table:
All chain must end with a top-level account
A chain may not have a circular reference
You have two ways to enforce this.
You can create a trigger in your database, and check the logic in the trigger. This has the benefit of running inside the database, so it applies to every transaction, regardless of the client. However, database triggers are not always popular. I see them as a side effect, and they can be hard to debug. Triggers run as part of your SQL, so if they are slow, your SQL will be slow.
The alternative is to enforce this logic in the application layer - whatever is talking to your database. This is easier to debug, and makes your business logic explicit to new developers - but it doesn't run inside the database, so you could end up replicating the logic if you have multiple client applications.
Here is an example that you could use as a basis to implement a database constraint that should prevent circular references in singular row updates; I don't believe this will work to prevent a circular reference if multiple rows are updated.
/*
ALTER TABLE dbo.Test DROP CONSTRAINT chkTest_PreventCircularRef
GO
DROP FUNCTION dbo.Test_PreventCircularRef
GO
DROP TABLE dbo.Test
GO
*/
CREATE TABLE dbo.Test (TestID INT PRIMARY KEY,TestID_Parent INT)
INSERT INTO dbo.Test(TestID,TestID_Parent) SELECT 1 AS TestID,NULL AS TestID_Parent
INSERT INTO dbo.Test(TestID,TestID_Parent) SELECT 2 AS TestID,1 AS TestID_Parent
INSERT INTO dbo.Test(TestID,TestID_Parent) SELECT 3 AS TestID,2 AS TestID_Parent
INSERT INTO dbo.Test(TestID,TestID_Parent) SELECT 4 AS TestID,3 AS TestID_Parent
INSERT INTO dbo.Test(TestID,TestID_Parent) SELECT 5 AS TestID,4 AS TestID_Parent
GO
GO
CREATE FUNCTION dbo.Test_PreventCircularRef (#TestID INT,#TestID_Parent INT)
RETURNS INT
BEGIN
--FOR TESTING:
--SELECT * FROM dbo.Test;DECLARE #TestID INT=3,#TestID_Parent INT=4
DECLARE #ParentID INT=#TestID
DECLARE #ChildID INT=NULL
DECLARE #RetVal INT=0
DECLARE #Ancestors TABLE(TestID INT)
DECLARE #Descendants TABLE(TestID INT)
--Get all descendants
INSERT INTO #Descendants(TestID) SELECT TestID FROM dbo.Test WHERE TestID_Parent=#TestID
WHILE (##ROWCOUNT>0)
BEGIN
INSERT INTO #Descendants(TestID)
SELECT t1.TestID
FROM dbo.Test t1
LEFT JOIN #Descendants relID ON relID.TestID=t1.TestID
WHERE relID.TestID IS NULL
AND t1.TestID_Parent IN (SELECT TestID FROM #Descendants)
END
--Get all ancestors
--INSERT INTO #Ancestors(TestID) SELECT TestID_Parent FROM dbo.Test WHERE TestID=#TestID
--WHILE (##ROWCOUNT>0)
--BEGIN
-- INSERT INTO #Ancestors(TestID)
-- SELECT t1.TestID_Parent
-- FROM dbo.Test t1
-- LEFT JOIN #Ancestors relID ON relID.TestID=t1.TestID_Parent
-- WHERE relID.TestID IS NULL
-- AND t1.TestID_Parent IS NOT NULL
-- AND t1.TestID IN (SELECT TestID FROM #Ancestors)
--END
--FOR TESTING:
--SELECT TestID AS [Ancestors] FROM #Ancestors;SELECT TestID AS [Descendants] FROM #Descendants;
IF EXISTS (
SELECT *
FROM #Descendants
WHERE TestID=#TestID_Parent
)
BEGIN
SET #RetVal=1
END
RETURN #RetVal
END
GO
ALTER TABLE dbo.Test
ADD CONSTRAINT chkTest_PreventCircularRef
CHECK (dbo.Test_PreventCircularRef(TestID,TestID_Parent) = 0);
GO
SELECT * FROM dbo.Test
--This is problematic as it creates a circular reference between TestID 3 and 4; it is now prevented
UPDATE dbo.Test SET TestID_Parent=4 WHERE TestID=3
Dealing with self-referencing tables / recursive relationships in SQL is not simple. I suppose this is evidenced by the fact that multiple people can't get their heads around the problem with just checking for single-depth cycles.
To enforce this with table constraints, you would need a check constraint based on a recursive query. At best that's DBMS-specific support, and it may not perform well if it has to be run on every update.
My advice is to have the code containing the UPDATE statement enforce this. That could take a couple of forms. In any case if it needs to be strictly enforced it may require limiting UPDATE access into the table to a service account used by a stored proc or external service.
Using a stored procedure would be vary similar to a CHECK constraint, except that you could use procedural (iterative) logic to look for cycles before doing the update. It has become unpopular to put too much logic in stored procs, though, and whether this type of check should be done is a judgement call from team to team / organization to organization.
Likewise using a service-based approach would let you use procedural logic to look for cycles, and you could write it in a language better suited to such logic. The issue here is, if services aren't part of your architecture then it's a bit heavy-weight to introduce a whole new layer. But, a service layer is probably considered more modern/popular (at the moment at least) than funneling updates through stored procs.
With those approaches in mind - and understanding that both procedural and recursive syntax in databases is DBMS-specific - there are too many possible syntax options to really go into. But the idea is:
Examine the proposed parent.
Check it's parent recursively
Do you ever reach the proposed child before reaching a top-level account? IF not, allow the update
Finally, I have created the scripts after some failures, its working fine for me.
-- To hold the Account table data
Declare #Accounts table (ID INT, ParentAccountID INT)
-- To be updated
Declare #AccountID int = 4;
Declare #ParentAccountID int = 7;
Declare #NextParentAccountID INT = #ParentAccountID
Declare #IsCircular int = 0
INSERT INTO #Accounts values (1, NULL), (2,1), (3,1) ,(4,3), (5,4), (6,5), (7,6), (8,7)
-- No circular reference value
--Select * from #Accounts
-- Request to update ParentAccountID to 7 for the Account ID 4
update #Accounts
set ParentAccountID = #ParentAccountID
where ID = #AccountID
Select * from #Accounts
WHILE(1=1)
BEGIN
-- Take the ParentAccountID for #NextParentAccountID
SELECT #NextParentAccountID = ParentAccountID from #Accounts WHERE ID = #NextParentAccountID
-- If the #NextParentAccountID is NULL, then it reaches the top level account, no circular reference hence break the loop
IF (#NextParentAccountID IS NULL)
BEGIN
BREAK;
END
-- If the #NextParentAccountID is equal to #AccountID (to which the update was done) then its creating circular reference
-- Then set the #IsCircular to 1 and break the loop
IF (#NextParentAccountID = #AccountID )
BEGIN
SET #IsCircular = 1
BREAK
END
END
IF #IsCircular = 1
BEGIN
select 'CircularReference' as 'ResponseCode'
END

sql server cannot access inserted table in a trigger

I am trying to create a simple to insert trigger that gets the count from a table and adds it to another like this
CREATE TABLE [poll-count](
id VARCHAR(100),
altid BIGINT,
option_order BIGINT,
uip VARCHAR(50),
[uid] VARCHAR(100),
[order] BIGINT
PRIMARY KEY NONCLUSTERED([order]),
FOREIGN KEY ([order]) references ord ([order]
)
GO
CREATE TRIGGER [get-poll-count]
ON [poll-count]
FOR INSERT
AS
BEGIN
DECLARE #count INT
SET #count = (SELECT COUNT (*) FROM [poll-count] WHERE option_order = i.option_order)
UPDATE [poll-options] SET [total] = #count WHERE [order] = i.option_order
END
GO
when i ever i try to run this i get this error:
The multi-part identifier "i.option_order" could not be bound
what is the problem?
thanks
Your trigger currently assumes that there will always be one-row inserts. Have you tried your trigger with anything like this?
INSERT dbo.[poll-options](option_order --, ...)
VALUES(1 --, ...),
(2 --, ...);
Also, you say that SQL Server "cannot access inserted table" - yet your statement says this. Where do you reference inserted (even if this were a valid subquery structure)?
SET #count = (SELECT COUNT (*) FROM [poll-count]
WHERE option_order = i.option_order)
-----------------------^ "i" <> "inserted"
Here is a trigger that properly references inserted and also properly handles multi-row inserts:
CREATE TRIGGER dbo.pollupdate
ON dbo.[poll-options]
FOR INSERT
AS
BEGIN
SET NOCOUNT ON;
;WITH x AS
(
SELECT option_order, c = COUNT(*)
FROM dbo.[poll-options] AS p
WHERE EXISTS
(
SELECT 1 FROM inserted
WHERE option_order = p.option_order
)
GROUP BY option_order
)
UPDATE p SET total = x.c
FROM dbo.[poll-options] AS p
INNER JOIN x
ON p.option_order = x.option_order;
END
GO
However, why do you want to store this data on every row? You can always derive the count at runtime, know that it is perfectly up to date, and avoid the need for a trigger altogether. If it's about the performance aspect of deriving the count at runtime, a much easier way to implement this write-ahead optimization for about the same maintenance cost during DML is to create an indexed view:
CREATE VIEW dbo.[poll-options-count]
WITH SCHEMABINDING
AS
SELECT option_order, c = COUNT_BIG(*)
FROM dbo.[poll-options]
GROUP BY option_order;
GO
CREATE UNIQUE CLUSTERED INDEX oo ON dbo.[poll-options-count](option_order);
GO
Now the index is maintained for you and you can derive very quick counts for any given (or all) option_order values. You'll have test, of course, whether the improvement in query time is worth the increased maintenance (though you are already paying that price with the trigger, except that it can affect many more rows in any given insert, so...).
As a final suggestion, don't use special characters like - in object names. It just forces you to always wrap it in [square brackets] and that's no fun for anyone.

MSSQL: Update statement avoiding the CHECK constraint

Working in MS2000, I have a table called JobOwners that maps Jobs (JPSID) to the Employees that own them (EmpID). It also contains the date they started owning that job (DateStarted), date they stopped owning that job (DateEnded) and if the ownership is active (IsActive). Looks like this.
CREATE TABLE JobOwners
(
LogID int NOT NULL IDENTITY(1,1) PRIMARY KEY,
JPSID int NOT NULL FOREIGN KEY REFERENCES JobsPerShift(JPSID),
EmpID int NOT NULL FOREIGN KEY REFERENCES Employees(EmpID),
DateStarted datetime,
DateEnded datetime,
IsActive tinyint NOT NULL
)
There should be no duplicates of JPSID that are active, although inactive duplicates should be fine. With some research I found I could accomplish this using a function on a CHECK constraint.
CREATE FUNCTION CheckActiveCount(#JPSID INT)
RETURNS INT AS
BEGIN
DECLARE #result INT
SELECT #result = COUNT(*) FROM JobOwners WHERE JPSID = #JPSID AND IsActive = 1
RETURN #result
END
GO
ALTER TABLE JobOwners
ADD CONSTRAINT CK_JobOwners_IsActive
CHECK ((IsActive = 1 AND dbo.CheckActiveCount(JPSID) <= 1) OR (IsActive = 0))
This works well enough. It will allow me to insert JPSID 2 with IsActive 1, as there is no other active JPSID 2. It will let me insert JPSID 2 with IsActive 0, because the check isn't applied when IsActive is 0. It rejects when I try to insert JPSID 2 with IsActive 1 again though, because it conflicts with the constraint. See below.
INSERT INTO JobOwners
VALUES(2,2,NULL,NULL,1)
(1 row(s) affected)
INSERT INTO JobOwners
VALUES(2,2,NULL,NULL,0)
(1 row(s) affected)
INSERT INTO JobOwners
VALUES(2,3,NULL,NULL,1)
INSERT statement conflicted with COLUMN FOREIGN KEY constraint...
The problem occurs if I try to update one of the inactive records to active. For some reason, it allows me.
UPDATE JobOwners SET IsActive = 1
WHERE LogID = 3
(1 row(s) affected)
If I run the same statement again, then it conflicts with the constraint, but not the first time. The front end of this app would never change an inactive record to active, it would just insert a new record, but it's still not something I'd like the table to allow.
I'm wondering if it might be best to separate the active job owners and have a seperate table for job owner history, but I'm not certain on the best practice here.
Any help would be greatly appreciated.
Thank you,
Ben
There is a known issue where certain operations will lead to a check constraint that calls a UDF to be bypassed. The bug was listed on Connect (before it was scuttled and all the links were orphaned) and it has been acknowledged, but closed as Won't Fix. This means we need to rely on workarounds.
My first workaround would probably be an instead of update trigger. Thanks to Martin for keeping me honest and for making me test this further - I found that I did not protect against two rows being updated to 1 in the same statement. I've corrected the logic and added a transaction to help prevent a race condition:
CREATE TRIGGER dbo.CheckJobOwners ON dbo.JobOwners
INSTEAD OF UPDATE
AS
BEGIN
SET NOCOUNT ON;
BEGIN TRANSACTION;
UPDATE j SET IsActive = 1 -- /* , other columns */
FROM dbo.JobOwners AS j INNER JOIN inserted AS i
ON i.LogID = j.LogID
WHERE i.IsActive = 1 AND NOT EXISTS
( -- since only one can be active, we don't need an expensive count:
SELECT 1 FROM dbo.JobOwners AS j2
WHERE j2.JPSID = i.JPSID
AND j2.IsActive = 1 AND j2.LogID <> i.LogID
)
AND NOT EXISTS
( -- also need to protect against two rows updated by same statement:
SELECT 1 FROM inserted AS i2
WHERE i2.JPSID = i.JPSID
AND i2.IsActive = 1 AND i2.LogID <> i.LogID
);
-- *if* you want to report errors:
IF (##ROWCOUNT <> (SELECT COUNT(*) FROM inserted WHERE IsActive = 1))
RAISERROR('At least one row was not updated.', 11, 1);
-- assume setting active = 0 always ok & that IsActive is not nullable
UPDATE j SET IsActive = 0 -- /* , other columns */
FROM dbo.JobOwners AS j INNER JOIN inserted AS i
ON j.LogID = i.LogID
WHERE i.IsActive = 0;
COMMIT TRANSACTION;
END
GO
(My only reason for an instead of instead of after trigger is that you only update the rows you need to update, instead of having to rollback after the fact (which won't let you only rollback the invalid updates in the case of a multi-row update)).
There is a lot of good discussion about this issue here:
https://web.archive.org/web/20171013131650/http://sqlblog.com/blogs/tibor_karaszi/archive/2009/12/17/be-careful-with-constraints-calling-udfs.aspx
EDIT: HUGE caveat. See Aaron's comment on this SO question for reasons you probably want to avoid combining UDFs and CHECK CONSTRAINTS. However, since (even after reading and understanding Aaron's concerns) my answer is still viable in our system because of 1) how our system works and 2) we actually want UPDATE statements to fail in the scenarios he describes, I am leaving my answer here. As it ALWAYS is, it is up to you to make sure you understand the ramifications of using the script in this answer. YOU HAVE BEEN WARNED
I followed the link in Aaron's (accepted) answer. In the description there was a specific piece of text that caught my attention "(to check values that are not passing as parameters)".
That gave me an idea. I have a table with columns CustomerId, ContactId, ContactType all of type "int". The PK is CustomerId and ContactId. I needed to be able to limit each CustomerId to only have one "Primary" Contact (ContactType = 1) but as many "secondary" and "other" contacts as people wanted to add. I had setup my UDF to accept only CustomerId as a parameter. So, I added ContactType as well but since I only cared about ContactType = 1, I just hard-coded the ContactType parameter to 1 inside the function. It worked on SQL2012 but I have no idea about other versions.
Here is a test script. I "squished" together some of the statements to reduce the amount of scrolling needed. Note: the constraint ALLOWS zero Primary Contacts because it would be impossible to set a different Contact as the Primary if you did not first remove an existing Primary.
CREATE TABLE [dbo].[CheckConstraintTest](
[CustomerId] [int] NOT NULL,
[ContactId] [int] NOT NULL,
[ContactType] [int] NULL,
CONSTRAINT [PK_CheckConstraintTest] PRIMARY KEY CLUSTERED (
[CustomerId] ASC,
[ContactId] ASC
))
GO
CREATE FUNCTION dbo.OnlyOnePrimaryContact (
#CustId int, #ContactType int ) RETURNS bit
AS BEGIN
DECLARE #result bit, #count int
SET #ContactType = 1 --only care about "1" but needed parm to force SQL to "care" about that column
SELECT #count = COUNT(*) FROM CheckConstraintTest WHERE [CustomerId] = #CustId AND [ContactType] = #ContactType
IF #count < 2 SET #result = 1
ELSE SET #result = 0
RETURN #result
END
GO
ALTER TABLE [dbo].[CheckConstraintTest] WITH CHECK ADD CONSTRAINT [SinglePrimaryContact] CHECK (([dbo].[OnlyOnePrimaryContact]([CustomerId],[ContactType])=(1)))
GO
ALTER TABLE [dbo].[CheckConstraintTest] CHECK CONSTRAINT [SinglePrimaryContact]
GO
INSERT INTO [CheckConstraintTest] (CustomerId, ContactId, ContactType)
VALUES (1,1,1), (1,2,2), (1,3,2), (1,4,2), (2,1,1)
INSERT INTO [CheckConstraintTest] (CustomerId, ContactId, ContactType)
VALUES (1,5,1) --This should fail
UPDATE [CheckConstraintTest] --This should fail
SET ContactType = 1
WHERE CustomerId = 1 AND ContactId = 2
UPDATE [CheckConstraintTest] --This should work
SET ContactType = 2
WHERE CustomerId = 1 AND ContactId = 1
INSERT INTO [CheckConstraintTest] (CustomerId, ContactId, ContactType)
VALUES (1,5,1) --This should work now since we change Cust 1, Contact 1, to "secondary" in previous statement

Does anyone know a neat trick for reusing identity values?

Typically when you specify an identity column you get a convenient interface in SQL Server for asking for particular row.
SELECT * FROM $IDENTITY = #pID
You don't really need to concern yourself with the name if the identity column because there can only be one.
But what if I have a table which mostly consists of temporary data. Lots of inserts and lots of deletes. Is there a simple way for me to reuse the identity values.
Preferably I would want to be able to write a function that would return say NEXT_SMALLEST($IDENTITY) as next identity value and do so in a fail-safe manner.
Basically find the smallest value that's not in use. That's not entirely trivial to do, but what I want is to be able to tell SQL Server that this is my function that will generate the identity values. But what I know is that no such function exists...
I want to...
Implement global data base IDs, I need to provide a default value that I'm in control of.
My idea was based around that I should be able to have a table with all known IDs and then every row ID from some other table that needed a global ID would reference that table. The default value would be provided by something like
INSERT INTO GlobalID
RETURN SCOPE_IDENTITY()
No; it's not unique if it can be reused.
Why do you want to re-use them? Why do you concern yourself with this field? If you want to be in control of it, don't make it an identity; create your own scheme and use that.
Don't reuse identities, you'll just shoot your self in the foot. Use a large enough value so that it never rolls over (64 bit big int).
To find missing gaps in a sequence of numbers join the table against itself with a +/- 1 difference:
SELECT a.id
FROM table AS a
LEFT OUTER JOIN table AS b ON a.id = b.id+1
WHERE b.id IS NULL;
This query will find the numbers in the id sequence for which id-1 is not in the table, ie. contiguous sequence start numbers. You can then use SET IDENTITY INSERT OFF to insert a specific id and reuse a number. The cost of doing so is overwhelming (both runtime and code complexity) compared with the an ordinary identity based insert.
If you really want to reset Identity value to the lowest,
here is the trick you can use through DBCC CHECKIDENT
Basically following sql statements resets identity value so that identity value restarts from the lowest possible number
create table TT (id int identity(1, 1))
GO
insert TT default values
GO 10
select * from TT
GO
delete TT where id between 5 and 10
GO
--; At this point, next ID will be 11, not 5
select * from TT
GO
insert TT default values
GO
--; as you can see here, next ID is indeed 11
select * from TT
GO
--; Now delete ID = 11
--; so that we can reseed next highest ID to 5
delete TT where id = 11
GO
--; Now, let''s reseed identity value to the lowest possible identity number
declare #seedID int
select #seedID = max(id) from TT
print #seedID --; 4
--; We reseed identity column with "DBCC CheckIdent" and pass a new seed value
--; But we can't pass a seed number as argument, so let's use dynamic sql.
declare #sql nvarchar(200)
set #sql = 'dbcc checkident(TT, reseed, ' + cast(#seedID as varchar) + ')'
exec sp_sqlexec #sql
GO
--; Now the next
insert TT default values
GO
--; as you can see here, next ID is indeed 5
select * from TT
GO
I guess we would really need to know why you want to reuse your identity column. The only reason I can think of is because of the temporary nature of your data you might exhaust the possible values for the identity. That is not really likely, but if that is your concern, you can use uniqueidentifiers (guids) as the primary key in your table instead.
The function newid() will create a new guid and can be used in insert statements (or other statements). Then when you delete the row, you don't have any "holes" in your key because guids are not created in that order anyway.
[Syntax assumes SQL2008....]
Yes, it's possible. You need to two management tables, and two triggers on each participating table.
First, the management tables:
-- this table should only ever have one row
CREATE TABLE NextId (Id INT)
INSERT NextId VALUES (1)
GO
CREATE TABLE RecoveredIds (Id INT NOT NULL PRIMARY KEY)
GO
Then, the triggers, two on each table:
CREATE TRIGGER tr_TableName_RecoverId ON TableName
FOR DELETE AS BEGIN
IF ##ROWCOUNT = 0 RETURN
INSERT RecoveredIds (Id) SELECT Id FROM deleted
END
GO
CREATE TRIGGER tr_TableName_AssignId ON TableName
INSTEAD OF INSERT AS BEGIN
DECLARE #rowcount INT = ##ROWCOUNT
IF #rowcount = 0 RETURN
DECLARE #required INT = #rowcount
DECLARE #new_ids TABLE (Id INT PRIMARY KEY)
DELETE TOP (#required) OUTPUT DELETED.Id INTO #new_ids (Id) FROM RecoveredIds
SET #rowcount = ##ROWCOUNT
IF #rowcount < #required BEGIN
DECLARE #output TABLE (Id INT)
UPDATE NextId SET Id = Id + (#required-#rowcount)
OUTPUT DELETED.Id INTO #output
-- this assumes you have a numbers table around somewhere
INSERT #new_ids (Id)
SELECT n.Number+o.Id-1 FROM Numbers n, #output o
WHERE n.Number BETWEEN 1 AND #required-#rowcount
END
SET IDENTITY_INSERT TableName ON
;WITH inserted_CTE AS (SELECT _no = ROW_NUMBER() OVER (ORDER BY Id), * FROM inserted)
, new_ids_CTE AS (SELECT _no = ROW_NUMBER() OVER (ORDER BY Id), * FROM #new_ids)
INSERT TableName (Id, Attr1, Attr2)
SELECT n.Id, i.Attr1, i.Attr2
FROM inserted_CTE i JOIN new_ids_CTE n ON i._no = n._no
SET IDENTITY_INSERT TableName OFF
END
You could script the triggers out easily enough from system tables.
You would want to test this for concurrency. It should work as is, syntax errors notwithstanding: The OUTPUT clause guarantees atomicity of id lookup->increment as one step, and the entire operation occurs within a transaction, thanks to the trigger.
TableName.Id is still an identity column. All the common idioms like $IDENTITY and SCOPE_IDENTITY() will still work.
There is no central table of ids by table, but you could create one easily enough.
I don't have any help for finding the values not in use but if you really want to find them and set them yourself, you can use
set identity_insert on ....
in your code to do so.
I'm with everyone else though. Why bother? Don't you have a business problem to solve?