SQL unique constraint on either of 2 columns - sql

I have a table in SQL that I would like to have a unique constraint so that any of two values cannot already exist.
for example if I have 2 columns, I would like it to not insert if the value in column B does not exist in column A or column B.
Is this possible and if so how is it done?
example:
Column A | Column B
--------------------
4 | 6
I would want any object that tries to insert 4 or 6 not to be allowed into the table

Trigger with ROLLBACK TRANSACTION is the way to go.
create trigger dbo.something after insert as
begin
if exists ( select * from inserted where ...check here if your data already exists... )
begin
rollback transaction
raiserror ('some message', 16, 1)
end
end

You can create a function which takes in these values & create a check constraint on it (referencing your functions return values) to your table.
create table t11 (Code int, code2 int)
create function fnCheckValues (#Val1 int, #Val2 int)
Returns int /*YOu can write a better implementation*/
as
Begin
DECLARE #CntRow int
IF(#Val1 IS NULL OR #Val2 IS NULL) RETURN 0
select #CntRow = count(*) from t11
where Code in (#Val1,#Val2 ) or Code2 in (#Val1,#Val2 )
RETURN #CntRow
End
GO
alter table t11 Add constraint CK_123 check ([dbo].[fnCheckValues]([Code],[code2])<=(1))

When one want to enforce a multi-row constraint that is not offered by the database engine, the obvious solution is use of a trigger or stored procedure. This often does not work because the database isolates the transactions the triggers and stored procedures run in, allowing violations in the presense of concurrency.
Instead turn the constraint into something that the database engine will enforce.
CREATE TABLE dbo.T (A INT, B INT)
GO
CREATE TABLE dbo.T_Constraint_Helper (ColumnName sysname PRIMARY KEY)
INSERT INTO dbo.T_Constraint_Helper (ColumnName)
VALUES ('A'), ('B')
GO
CREATE VIEW T_Constraint_VW
WITH SCHEMABINDING AS
SELECT CASE CH.ColumnName WHEN 'A' THEN T.A ELSE T.B END AS Value
FROM dbo.T
CROSS JOIN dbo.T_Constraint_Helper CH
GO
CREATE UNIQUE CLUSTERED INDEX FunnyConstraint_VW_UK ON dbo.T_Constraint_VW (Value)
GO
INSERT INTO T VALUES (1, 2)
-- works
INSERT INTO T VALUES (2, 3)
-- Msg 2601, Level 14, State 1, Line 1
-- Cannot insert duplicate key row in object 'dbo.T_Constraint_VW' with unique index 'T_Constraint_VW_UK'. The duplicate key value is (2).
INSERT INTO T VALUES (4, 4)
-- Msg 2601, Level 14, State 1, Line 1
-- Cannot insert duplicate key row in object 'dbo.T_Constraint_VW' with unique index 'T_Constraint_VW_UK'. The duplicate key value is (4).
INSERT INTO T VALUES (5, 6)
-- works

Related

How to create trigger that updates field on parent with the sum from 2 children

Updated to include screenshot - I need to create a trigger to update a field on a parent table with the sum of the values from two child tables. When the parent record is saved it should calculate ParentTotalEmployees = Sum(CountryTotEmployees) + Sum(StateTotEmployees). I can get it to populate if I only reference one child table but I haven't been able to figure out how to include the second child table.
ALTER TRIGGER [dbo].[DD_UpdateTotEmp] ON [dbo].[DEALDATA]
AFTER INSERT,DELETE,UPDATE
AS
BEGIN
;WITH GrandTotCountry AS (
SELECT c.QDEALDATA1,
SUM(QTOTCOUNTRYEMP) AS TotCountryEmp
FROM
DEALDATA1 c
GROUP BY c.QDEALDATA1
),
GrandTotState AS (
SELECT c.QDEALDATA,
SUM(QNUMSTATEEMP) AS TotStateEmp
FROM
DEALDATA2 c
GROUP BY c.QDEALDATA)
UPDATE T1
SET T1.QGRANDTOTEMP = (SELECT TotCountryEmp
FROM GrandTotCountry T2
WHERE T2.QDEALDATA=i.QDEALDATA)
FROM DEALDATA T1
INNER JOIN Inserted i ON T1.QDEALDATA=i.QDEALDATA
END
OR THIS ONE
CREATE TRIGGER [dbo].[DD_UpdateTotEmp] ON [dbo].[DEALDATA]
AFTER INSERT,DELETE,UPDATE
AS
BEGIN
UPDATE T1
SET T1.QGRANDTOTEMP = (SELECT SUM(QTOTCOUNTRYEMP)
FROM DEALDATA1 T2
WHERE T2.QDEALDATA=i.QDEALDATA)
FROM DEALDATA T1
INNER JOIN Inserted i ON T1.QDEALDATA=i.QDEALDATA
END
Sample Data
USE TEMPDB
GO
-- Parent Table
CREATE TABLE [dbo].[DEALDATA](
[QDEALDATA] [varchar](36) NOT NULL PRIMARY KEY CLUSTERED,
[MATTERSYSID] [varchar](36) NULL,
[QGRANDTOTEMP] [numeric](12, 0) NULL )
GO
INSERT INTO DEALDATA VALUES ('1404fcb1','C333897E',NULL);
INSERT INTO DEALDATA VALUES ('a51f9f8a','8AE3F809',NULL);
GO
-- Country Emp Table
CREATE TABLE [dbo].[DEALDATA1](
[QDEALDATA1] [varchar](36) NOT NULL PRIMARY KEY CLUSTERED,
[QDEALDATA] [varchar](36) NULL,
[QCOUNTRY] [varchar](40) NULL,
[QTOTCOUNTRYEMP] [numeric](12, 0) NULL )
GO
INSERT INTO DEALDATA1 VALUES ('60ae5737','a51f9f8a','Monaco',5);
INSERT INTO DEALDATA1 VALUES ('62ceecb9','a51f9f8a','Australia',10);
INSERT INTO DEALDATA1 VALUES ('a645fcd1','1404fcb1','United States',100);
GO
-- State Emp Table
CREATE TABLE [dbo].[DEALDATA2](
[QDEALDATA2] [varchar](36) NOT NULL PRIMARY KEY CLUSTERED,
[QDEALDATA] [varchar](36) NULL,
[QEMPSTATE] [varchar](40) NULL,
[QNUMSTATEEMP] [numeric](12, 0) NULL )
GO
INSERT INTO DEALDATA2 VALUES ('453b7b64','a51f9f8a','NY',50);
INSERT INTO DEALDATA2 VALUES ('e803b38f','a51f9f8a','KY',50);
INSERT INTO DEALDATA2 VALUES ('413954e1','1404fcb1','MO',20);
INSERT INTO DEALDATA2 VALUES ('ef2213e5','1404fcb1','HI',10);
GO
Thank you in advance in helping me with this.
A trigger (insert, Update, and/or Delete) belongs to a particular table. If you need a trigger on two tables (or many tables) you will need two triggers (or many triggers).
However, you can write a stored-procedure and call it from two triggers. And Since you have used after trigger, you don't need to use Inserted, Deleted objects.
It can be like this:
ALTER TRIGGER Trigger1 ON Table1
AFTER INSERT,DELETE,UPDATE
AS
BEGIN
EXEC‌ TheProcedure
END‌
and
ALTER TRIGGER Trigger2 ON Table2
AFTER INSERT,DELETE,UPDATE
AS
BEGIN
EXEC‌ TheProcedure
END‌
As you see the notes bellow, the above code performance is really bad. The best you can do is to redesign your tables. However, if you prefer slight modification on your data base design, you can create two aggregate tables for your child tables and then use a VIEW‌ to combine them into a single result.
Here is the solution. Thanks to all who responded!
UPDATE dst
SET dst.GrandTotEmp = COALESCE(tot1.TotCountryEmp, 0) + COALESCE(tot2.TotStateEmp, 0)
FROM DEALDATA as dst
JOIN inserted AS i ON dst.QDEALDATA = i.QDEALDATA
LEFT JOIN GrandTotCountry AS tot1 ON tot1.QDEALDATA = dst.QDEALDATA
LEFT JOIN GrantTotState AS tot2 ON tot2.QDEALDATA = dst.QDEALDATA

Prohibit Inserting Rows With Default Constraint

Is there any way of how to prevent inserting data in specified columns in table and use only the default (constraint) values?
E.g. I have columns:
LogInsert (DF GETDATE())
LogUser (DF ORIGINAL_LOGIN())
both defined with DEFAULT constraint. I do not want to allow users to insert into those columns, but use default values here instead when inserting new row.
This should raise an error.
INSERT INTO T1
( C1
,C2
,LogInsert
,LogUser
)
VALUES ( 'A'
,'B'
,'20160101 10:53'
,'domain\user'
);
User should be able to perform the following script without error.
INSERT INTO T1
( C1, C2 )
VALUES ( 'A', 'B' );
You could always give your users a view to work against instead of a table. You can then either choose to hide the columns completely or (as here) make them computed so that they cannot insert a value into the column, via the view:
create table dbo._T1 (
ID int IDENTITY(1,1) not null,
Inserted datetime2 constraint DF__T1_Inserted DEFAULT (SYSDATETIME()) not null,
ABC varchar(10) not null,
constraint PK__T1 PRIMARY KEY (ID)
)
go
create view dbo.T1
with schemabinding
as
select
ID,
COALESCE(Inserted,SYSDATETIME()) as Inserted,
ABC
from dbo._T1
go
insert into dbo.T1 (ABC) values ('abc')
go
insert into dbo.T1 (ABC,Inserted) values ('def',SYSDATETIME())
Results:
(1 row(s) affected)
Msg 4406, Level 16, State 1, Line 19
Update or insert of view or function 'dbo.T1' failed because it contains a derived or constant field.
All of the users queries continue to just use T1. It just happens to be a view rather than a table.
In the above, the view uses COALESCE(Inserted,SYSDATETIME()). It doesn't really matter what's used here, and it doesn't need to match e.g. the default definition. All that's important is that some computation is performed on the Inserted column so that it becomes a read-only column in the view.
You can create a Check Constraint on the table, for example the below
CREATE TABLE [dbo].[T1]
(
[C1] VARCHAR(50)
,[LogInsert] DATETIME DEFAULT GETDATE()
,[LogUser] VARCHAR(500) DEFAULT ORIGINAL_LOGIN()
)
ALTER TABLE [T1] WITH CHECK ADD CONSTRAINT [CK_T1_LogInsert] CHECK ([LogInsert] = GETDATE())
ALTER TABLE [T1] WITH CHECK ADD CONSTRAINT [CK_T1_LogUser] CHECK ([LogUser] = ORIGINAL_LOGIN())
INSERT INTO [dbo].[T1] ([C1]) VALUES ('A')
INSERT INTO [dbo].[T1] ([C1]) VALUES ('B')
INSERT INTO [dbo].[T1] ([C1]) VALUES ('C')
SELECT * FROM [dbo].[T1]
--Will Fail
INSERT INTO [dbo].[T1] ([C1],[LogInsert]) VALUES ('D','2016-11-11 00:00')
INSERT INTO [dbo].[T1] ([C1],[LogUser]) VALUES ('D','Not Your UserName')
OR
You can force the user to only insert using a stored procedure and not allow that as a parameter, this can be done with a table variable too for bulk inserts.

Add unique constraint to combination of two columns

I have a table and, somehow, the same person got into my Person table twice. Right now, the primary key is just an autonumber but there are two other fields that exist that I want to force to be unique.
For example, the fields are:
ID
Name
Active
PersonNumber
I only want 1 record with a unique PersonNumber and Active = 1.
(So the combination of the two fields needs to be unique)
What is the best way on an existing table in SQL server I can make it so if anyone else does an insert with the same value as an existing value, it fails so I don't have to worry about this in my application code.
Once you have removed your duplicate(s):
ALTER TABLE dbo.yourtablename
ADD CONSTRAINT uq_yourtablename UNIQUE(column1, column2);
or
CREATE UNIQUE INDEX uq_yourtablename
ON dbo.yourtablename(column1, column2);
Of course, it can often be better to check for this violation first, before just letting SQL Server try to insert the row and returning an exception (exceptions are expensive).
Performance impact of different error handling techniques
Checking for potential constraint violations before entering TRY/CATCH
If you want to prevent exceptions from bubbling up to the application, without making changes to the application, you can use an INSTEAD OF trigger:
CREATE TRIGGER dbo.BlockDuplicatesYourTable
ON dbo.YourTable
INSTEAD OF INSERT
AS
BEGIN
SET NOCOUNT ON;
IF NOT EXISTS (SELECT 1 FROM inserted AS i
INNER JOIN dbo.YourTable AS t
ON i.column1 = t.column1
AND i.column2 = t.column2
)
BEGIN
INSERT dbo.YourTable(column1, column2, ...)
SELECT column1, column2, ... FROM inserted;
END
ELSE
BEGIN
PRINT 'Did nothing.';
END
END
GO
But if you don't tell the user they didn't perform the insert, they're going to wonder why the data isn't there and no exception was reported.
EDIT here is an example that does exactly what you're asking for, even using the same names as your question, and proves it. You should try it out before assuming the above ideas only treat one column or the other as opposed to the combination...
USE tempdb;
GO
CREATE TABLE dbo.Person
(
ID INT IDENTITY(1,1) PRIMARY KEY,
Name NVARCHAR(32),
Active BIT,
PersonNumber INT
);
GO
ALTER TABLE dbo.Person
ADD CONSTRAINT uq_Person UNIQUE(PersonNumber, Active);
GO
-- succeeds:
INSERT dbo.Person(Name, Active, PersonNumber)
VALUES(N'foo', 1, 22);
GO
-- succeeds:
INSERT dbo.Person(Name, Active, PersonNumber)
VALUES(N'foo', 0, 22);
GO
-- fails:
INSERT dbo.Person(Name, Active, PersonNumber)
VALUES(N'foo', 1, 22);
GO
Data in the table after all of this:
ID Name Active PersonNumber
---- ------ ------ ------------
1 foo 1 22
2 foo 0 22
Error message on the last insert:
Msg 2627, Level 14, State 1, Line 3
Violation of UNIQUE KEY constraint 'uq_Person'. Cannot insert duplicate key in object 'dbo.Person'.
The statement has been terminated.
Also I blogged more recently about a solution to applying a unique constraint to two columns in either order:
Enforce a Unique Constraint Where Order Does Not Matter
This can also be done in the GUI:
Under the table "Person", right click Indexes
Click/hover New Index
Click Non-Clustered Index...
A default Index name will be given but you may want to change it.
Check Unique checkbox
Click Add... button
Check the columns you want included
Click OK in each window.
In my case, I needed to allow many inactives and only one combination of two keys active, like this:
UUL_USR_IDF UUL_UND_IDF UUL_ATUAL
137 18 0
137 19 0
137 20 1
137 21 0
This seems to work:
CREATE UNIQUE NONCLUSTERED INDEX UQ_USR_UND_UUL_USR_IDF_UUL_ATUAL
ON USER_UND(UUL_USR_IDF, UUL_ATUAL)
WHERE UUL_ATUAL = 1;
Here are my test cases:
SELECT * FROM USER_UND WHERE UUL_USR_IDF = 137
insert into USER_UND values (137, 22, 1) --I CAN NOT => Cannot insert duplicate key row in object 'dbo.USER_UND' with unique index 'UQ_USR_UND_UUL_USR_IDF_UUL_ATUAL'. The duplicate key value is (137, 1).
insert into USER_UND values (137, 23, 0) --I CAN
insert into USER_UND values (137, 24, 0) --I CAN
DELETE FROM USER_UND WHERE UUL_USR_ID = 137
insert into USER_UND values (137, 22, 1) --I CAN
insert into USER_UND values (137, 27, 1) --I CAN NOT => Cannot insert duplicate key row in object 'dbo.USER_UND' with unique index 'UQ_USR_UND_UUL_USR_IDF_UUL_ATUAL'. The duplicate key value is (137, 1).
insert into USER_UND values (137, 28, 0) --I CAN
insert into USER_UND values (137, 29, 0) --I CAN
And if you have lot insert queries but not wanna ger a ERROR message everytime , you can do it:
CREATE UNIQUE NONCLUSTERED INDEX SK01 ON dbo.Person(ID,Name,Active,PersonNumber)
WITH(IGNORE_DUP_KEY = ON)

How to add data to two tables linked via a foreign key?

If I were to have 2 tables, call them TableA and TableB. TableB contains a foreign key which refers to TableA. I now need to add data to both TableA and TableB for a given scenario. To do this I first have to insert data in TableA then find and retrieve TableA's last inserted primary key and use it as the foreign key value in TableB. I then insert values in TableB. This seems lika a bit to much of work just to insert 1 set of data. How else can I achieve this? If possible please provide me with SQL statements for SQL Server 2005.
That sounds about right. Note that you can use SCOPE_IDENTITY() on a per-row basis, or you can do set-based operations if you use the INSERT/OUTPUT syntax, and then join the the set of output from the first insert - for example, here we only have 1 INSERT (each) into the "real" tables:
/*DROP TABLE STAGE_A
DROP TABLE STAGE_B
DROP TABLE B
DROP TABLE A*/
SET NOCOUNT ON
CREATE TABLE STAGE_A (
CustomerKey varchar(10),
Name varchar(100))
CREATE TABLE STAGE_B (
CustomerKey varchar(10),
OrderNumber varchar(100))
CREATE TABLE A (
Id int NOT NULL IDENTITY(51,1) PRIMARY KEY,
CustomerKey varchar(10),
Name varchar(100))
CREATE TABLE B (
Id int NOT NULL IDENTITY(1123,1) PRIMARY KEY,
CustomerId int,
OrderNumber varchar(100))
ALTER TABLE B ADD FOREIGN KEY (CustomerId) REFERENCES A(Id);
INSERT STAGE_A VALUES ('foo', 'Foo Corp')
INSERT STAGE_A VALUES ('bar', 'Bar Industries')
INSERT STAGE_B VALUES ('foo', '12345')
INSERT STAGE_B VALUES ('foo', '23456')
INSERT STAGE_B VALUES ('bar', '34567')
DECLARE #CustMap TABLE (CustomerKey varchar(10), Id int NOT NULL)
INSERT A (CustomerKey, Name)
OUTPUT INSERTED.CustomerKey,INSERTED.Id INTO #CustMap
SELECT CustomerKey, Name
FROM STAGE_A
INSERT B (CustomerId, OrderNumber)
SELECT map.Id, b.OrderNumber
FROM STAGE_B b
INNER JOIN #CustMap map ON map.CustomerKey = b.CustomerKey
SELECT * FROM A
SELECT * FROM B
If you work directly with SQL you have the right solution.
In case you're performing the insert from code, you may have higher level structures that help you achieve this (LINQ, Django Models, etc).
If you are going to do this in direct SQL, I suggest creating a stored procedure that takes all of the data as parameters, then performs the insert/select identity/insert steps inside a transaction. Even though the process is still the same as your manual inserts, using the stored procedure will allow you to more easily use it from your code. As #Rax mentions, you may also be able to use an ORM to get similar functionality.

conditional unique constraint

I have a situation where i need to enforce a unique constraint on a set of columns, but only for one value of a column.
So for example I have a table like Table(ID, Name, RecordStatus).
RecordStatus can only have a value 1 or 2 (active or deleted), and I want to create a unique constraint on (ID, RecordStatus) only when RecordStatus = 1, since I don't care if there are multiple deleted records with the same ID.
Apart from writing triggers, can I do that?
I am using SQL Server 2005.
Behold, the filtered index. From the documentation (emphasis mine):
A filtered index is an optimized nonclustered index especially suited to cover queries that select from a well-defined subset of data. It uses a filter predicate to index a portion of rows in the table. A well-designed filtered index can improve query performance as well as reduce index maintenance and storage costs compared with full-table indexes.
And here's an example combining a unique index with a filter predicate:
create unique index MyIndex
on MyTable(ID)
where RecordStatus = 1;
This essentially enforces uniqueness of ID when RecordStatus is 1.
Following the creation of that index, a uniqueness violation will raise an arror:
Msg 2601, Level 14, State 1, Line 13
Cannot insert duplicate key row in object 'dbo.MyTable' with unique index 'MyIndex'. The duplicate key value is (9999).
Note: the filtered index was introduced in SQL Server 2008. For earlier versions of SQL Server, please see this answer.
Add a check constraint like this. The difference is, you'll return false if Status = 1 and Count > 0.
http://msdn.microsoft.com/en-us/library/ms188258.aspx
CREATE TABLE CheckConstraint
(
Id TINYINT,
Name VARCHAR(50),
RecordStatus TINYINT
)
GO
CREATE FUNCTION CheckActiveCount(
#Id INT
) RETURNS INT AS BEGIN
DECLARE #ret INT;
SELECT #ret = COUNT(*) FROM CheckConstraint WHERE Id = #Id AND RecordStatus = 1;
RETURN #ret;
END;
GO
ALTER TABLE CheckConstraint
ADD CONSTRAINT CheckActiveCountConstraint CHECK (NOT (dbo.CheckActiveCount(Id) > 1 AND RecordStatus = 1));
INSERT INTO CheckConstraint VALUES (1, 'No Problems', 2);
INSERT INTO CheckConstraint VALUES (1, 'No Problems', 2);
INSERT INTO CheckConstraint VALUES (1, 'No Problems', 2);
INSERT INTO CheckConstraint VALUES (1, 'No Problems', 1);
INSERT INTO CheckConstraint VALUES (2, 'Oh no!', 1);
INSERT INTO CheckConstraint VALUES (2, 'Oh no!', 2);
-- Msg 547, Level 16, State 0, Line 14
-- The INSERT statement conflicted with the CHECK constraint "CheckActiveCountConstraint". The conflict occurred in database "TestSchema", table "dbo.CheckConstraint".
INSERT INTO CheckConstraint VALUES (2, 'Oh no!', 1);
SELECT * FROM CheckConstraint;
-- Id Name RecordStatus
-- ---- ------------ ------------
-- 1 No Problems 2
-- 1 No Problems 2
-- 1 No Problems 2
-- 1 No Problems 1
-- 2 Oh no! 1
-- 2 Oh no! 2
ALTER TABLE CheckConstraint
DROP CONSTRAINT CheckActiveCountConstraint;
DROP FUNCTION CheckActiveCount;
DROP TABLE CheckConstraint;
You could move the deleted records to a table that lacks the constraint, and perhaps use a view with UNION of the two tables to preserve the appearance of a single table.
You can do this in a really hacky way...
Create an schemabound view on your table.
CREATE VIEW Whatever
SELECT * FROM Table
WHERE RecordStatus = 1
Now create a unique constraint on the view with the fields you want.
One note about schemabound views though, if you change the underlying tables you will have to recreate the view. Plenty of gotchas because of that.
For those still searching for a solution, I came accross a nice answer, to a similar question and I think this can be still useful for many. While moving deleted records to another table may be a better solution, for those who don't want to move the record can use the idea in the linked answer which is as follows.
Set deleted=0 when the record is available/active.
Set deleted=<row_id or some other unique value> when marking the row
as deleted.
If you can't use NULL as a RecordStatus as Bill's suggested, you could combine his idea with a function-based index. Create a function that returns NULL if the RecordStatus is not one of the values you want to consider in your constraint (and the RecordStatus otherwise) and create an index over that.
That'll have the advantage that you don't have to explicitly examine other rows in the table in your constraint, which could cause you performance issues.
I should say I don't know SQL server at all, but I have successfully used this approach in Oracle.
Because, you are going to allow duplicates, a unique constraint will not work. You can create a check constraint for RecordStatus column and a stored procedure for INSERT that checks the existing active records before inserting duplicate IDs.