How to add a unique constraint on several columns, with a condition? - sql

Question:
I want to add a unique constraint on a mapping table (n:n).
I want that new values may be inserted, but only if TEST_FK_UID, TEST_DateFrom and TEST_DateTo are not equal to an already existing entry.
The problem is the status field.
Status 1 means active..
Status != 1 means inactive/deleted..
..
So one may of course insert a new entry with the same FK, DateFrom and DateTo, IF - and only if - the status of the existing entry (all existing entries, as you can insert, delete, insert, delete, insert, delete, etc.) is != 1
Here is what I have so far:
CREATE TABLE dbo._________Test
(
TEST_UID uniqueidentifier NOT NULL
,TEST_FK_UID uniqueidentifier NOT NULL
,TEST_DateFrom DateTime NOT NULL
,TEST_DateTo DateTime NOT NULL
,TEST_Status int NOT NULL
,UNIQUE(TEST_FK_UID, TEST_DateFrom, TEST_DateTo, TEST_Status)
);

You cannot. You can, however, create a unique index. It functions similarly, and I expect well enough for you.
CREATE UNIQUE INDEX MyIndex
ON _________Test
( TEST_FK_UID
, TEST_DateFrom
, TEST_DateTo )
WHERE TEST_Status = 1
The most important difference between a unique index and a unique constraint is that you cannot create a foreign key in another table that references a unique index. Edit: as Martin points out, this is not true, a foreign key can reference a nonfiltered unique index.

Use Instead Of trigger on INSERT,UPDATE operations..
and check the existing values with the values in the INSERTED table(which is created in the case of triggers)
If the status in the INSERTED table id 1 AND if it is unique, do the insertion operation or just abort with some messages..

It is very possible, like this
(basic credit goes to: https://stackoverflow.com/users/103075):
Edit:
OK, pedantically seen it's not a unique constraint, it's a check constraint, but WTF - it has the same effect and works on SQL-Server 2005 as well, and the (conditional) condition is configurable per customer (replace SET #bNoCheckForThisCustomer = 'false' with a select to a configuration table) - that's not possible with a unique index AFAIK ... ;)
Note this line:
AND ZO_RMMIO_UID != #in_ZO_RMMIO_UID
(ZO_RMMIO_UID is the unique primary key of the n:n mapping table)
It's important, since a check constraint seems to be similar to a onAfterInsert trigger.
If this line is missing, it checks on itselfs as well, which leads to the function always returning true...
IF EXISTS (SELECT * FROM sys.check_constraints WHERE object_id = OBJECT_ID(N'[dbo].[CheckNoDuplicate_T_ZO_AP_Raum_AP_Ref_Mietobjekt]') AND parent_object_id = OBJECT_ID(N'[dbo].[T_ZO_AP_Raum_AP_Ref_Mietobjekt]'))
ALTER TABLE [dbo].[T_ZO_AP_Raum_AP_Ref_Mietobjekt] DROP CONSTRAINT [CheckNoDuplicate_T_ZO_AP_Raum_AP_Ref_Mietobjekt]
GO
IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[fu_InsertCheck_IsDuplicate_T_ZO_AP_Raum_AP_Ref_Mietobjekt]') AND type in (N'FN', N'IF', N'TF', N'FS', N'FT'))
DROP FUNCTION [dbo].[fu_InsertCheck_IsDuplicate_T_ZO_AP_Raum_AP_Ref_Mietobjekt]
GO
-- ========================================================================
-- Author: Me
-- Create date: 09.08.2010
-- Last modified: 09.08.2010
-- Description: Conditionally check if row is a duplicate
-- ========================================================================
-- PRE: UID, Valid RM_UID, Valid MIO_UID,
-- Valid datetime-from for db usr language, valid datetime-to for db usr language
-- POST: True/False
CREATE FUNCTION [dbo].[fu_InsertCheck_IsDuplicate_T_ZO_AP_Raum_AP_Ref_Mietobjekt](#in_ZO_RMMIO_UID uniqueidentifier, #in_ZO_RMMIO_RM_UID AS uniqueidentifier, #in_ZO_RMMIO_MIO_UID as uniqueidentifier, #in_ZO_RMMIO_DatumVon AS datetime, #in_ZO_RMMIO_DatumBis AS datetime)
RETURNS bit
AS
BEGIN
DECLARE #bIsDuplicate AS bit
SET #bIsDuplicate = 'false'
DECLARE #bNoCheckForThisCustomer AS bit
SET #bNoCheckForThisCustomer = 'false'
IF #bNoCheckForThisCustomer = 'true'
RETURN #bIsDuplicate
IF EXISTS
(
SELECT
ZO_RMMIO_UID
,ZO_RMMIO_RM_UID
,ZO_RMMIO_MIO_UID
FROM T_ZO_AP_Raum_AP_Ref_Mietobjekt
WHERE ZO_RMMIO_Status = 1
AND ZO_RMMIO_UID != #in_ZO_RMMIO_UID
AND ZO_RMMIO_RM_UID = #in_ZO_RMMIO_RM_UID
AND ZO_RMMIO_MIO_UID = #in_ZO_RMMIO_MIO_UID
AND ZO_RMMIO_DatumVon = #in_ZO_RMMIO_DatumVon
AND ZO_RMMIO_DatumBis = #in_ZO_RMMIO_DatumBis
)
SET #bIsDuplicate = 'true'
RETURN #bIsDuplicate
END
GO
ALTER TABLE [dbo].[T_ZO_AP_Raum_AP_Ref_Mietobjekt] WITH NOCHECK ADD CONSTRAINT [CheckNoDuplicate_T_ZO_AP_Raum_AP_Ref_Mietobjekt]
CHECK
(
NOT
(
dbo.fu_InsertCheck_IsDuplicate_T_ZO_AP_Raum_AP_Ref_Mietobjekt(ZO_RMMIO_UID, ZO_RMMIO_RM_UID, ZO_RMMIO_MIO_UID, ZO_RMMIO_DatumVon, ZO_RMMIO_DatumBis) = 1
)
)
GO
ALTER TABLE [dbo].[T_ZO_AP_Raum_AP_Ref_Mietobjekt] CHECK CONSTRAINT [CheckNoDuplicate_T_ZO_AP_Raum_AP_Ref_Mietobjekt]
GO
And here a test case:
CREATE TABLE [dbo].[T_ZO_AP_Raum_AP_Ref_Mietobjekt](
[ZO_RMMIO_UID] [uniqueidentifier] NOT NULL, -- <== PRIMARY KEY
[ZO_RMMIO_RM_UID] [uniqueidentifier] NOT NULL,
[ZO_RMMIO_MIO_UID] [uniqueidentifier] NOT NULL,
[ZO_RMMIO_DatumVon] [datetime] NOT NULL,
[ZO_RMMIO_DatumBis] [datetime] NOT NULL,
[ZO_RMMIO_Status] [int] NOT NULL,
[ZO_RMMIO_Bemerkung] [text] NULL
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO
/*
DELETE FROM T_ZO_AP_Raum_AP_Ref_Mietobjekt
WHERE ZO_RMMIO_Status = 1
AND ZO_RMMIO_RM_UID = '2007B6F5-9010-4979-AB39-00057DA353C0'
AND ZO_RMMIO_MIO_UID = 'FFA177E9-971E-4500-805D-00116F708E7B'
*/
INSERT INTO T_ZO_AP_Raum_AP_Ref_Mietobjekt
(
ZO_RMMIO_UID
,ZO_RMMIO_RM_UID
,ZO_RMMIO_MIO_UID
,ZO_RMMIO_DatumVon
,ZO_RMMIO_DatumBis
,ZO_RMMIO_Status
,ZO_RMMIO_Bemerkung
)
VALUES
(
NEWID() --<ZO_RMMIO_UID, uniqueidentifier,>
,'2007B6F5-9010-4979-AB39-00057DA353C0' --<ZO_RMMIO_RM_UID, uniqueidentifier,>
,'FFA177E9-971E-4500-805D-00116F708E7B' --<ZO_RMMIO_MIO_UID, uniqueidentifier,>
,'01.01.2012' --<ZO_RMMIO_DatumVon, datetime,>
,'31.12.2999' --<ZO_RMMIO_DatumBis, datetime,>
,1 --<ZO_RMMIO_Status, int,>
,NULL--<ZO_RMMIO_Bemerkung, text,>
)
GO
INSERT INTO T_ZO_AP_Raum_AP_Ref_Mietobjekt
(
ZO_RMMIO_UID
,ZO_RMMIO_RM_UID
,ZO_RMMIO_MIO_UID
,ZO_RMMIO_DatumVon
,ZO_RMMIO_DatumBis
,ZO_RMMIO_Status
,ZO_RMMIO_Bemerkung
)
VALUES
(
NEWID() --<ZO_RMMIO_UID, uniqueidentifier,>
,'2007B6F5-9010-4979-AB39-00057DA353C0' --<ZO_RMMIO_RM_UID, uniqueidentifier,>
,'FFA177E9-971E-4500-805D-00116F708E7B' --<ZO_RMMIO_MIO_UID, uniqueidentifier,>
,'01.01.2012' --<ZO_RMMIO_DatumVon, datetime,>
,'31.12.2999' --<ZO_RMMIO_DatumBis, datetime,>
,1 --<ZO_RMMIO_Status, int,>
,NULL--<ZO_RMMIO_Bemerkung, text,>
)
GO
SELECT [ZO_RMMIO_UID]
,[ZO_RMMIO_RM_UID]
,[ZO_RMMIO_MIO_UID]
,[ZO_RMMIO_DatumVon]
,[ZO_RMMIO_DatumBis]
,[ZO_RMMIO_Status]
,[ZO_RMMIO_Bemerkung]
FROM [T_ZO_AP_Raum_AP_Ref_Mietobjekt]

Related

Data gets changed when copying data in chunks between two identical tables

In short, I am trying to copy data from one table to another nearly identical table (minus constraints, indices, and a precision change to a decimal column) in batches using Insert [NewTable] Select Top X * from [Table] but some data is getting changed during the copy. Read on for more details.
Why we are copying in the first place
We are altering the precision of a couple of columns in our largest table and do not have the time in our deployment window to do a simple alter statement. As an alternative, we decided to create a table with the new schema and copy the data in batches in the days leading up to the deploy to allow us to simple drop the old table and rename this table during the deployment window.
Creation scripts for new and old tables
These are not the exact tables we have in our DB, but they've been trimmed down for this question. The actual table has ~100 columns.
CREATE TABLE [dbo].[Table]
(
[Id] BIGINT NOT NULL PRIMARY KEY NONCLUSTERED IDENTITY,
[ForeignKey1] INT NOT NULL,
[ForeignKey2] INT NOT NULL,
[ForeignKey3] INT NOT NULL,
[Name] VARCHAR(MAX) NOT NULL,
[SomeValue] DECIMAL(14, 5) NULL,
CONSTRAINT [FK_Table_ForeignKeyTable1] FOREIGN KEY ([ForeignKey1]) REFERENCES [ForeignKeyTable1]([ForeignKey1]),
CONSTRAINT [FK_Table_ForeignKeyTable2] FOREIGN KEY ([ForeignKey2]) REFERENCES [ForeignKeyTable2]([ForeignKey2]),
CONSTRAINT [FK_Table_ForeignKeyTable3] FOREIGN KEY ([ForeignKey3]) REFERENCES [ForeignKeyTable3]([ForeignKey3]),
)
GO
CREATE INDEX [IX_Table_ForeignKey2] ON [dbo].[Table] ([ForeignKey2])
GO
CREATE TABLE [dbo].[NewTable]
(
[Id] BIGINT NOT NULL PRIMARY KEY NONCLUSTERED IDENTITY,
[ForeignKey1] INT NOT NULL,
[ForeignKey2] INT NOT NULL,
[ForeignKey3] INT NOT NULL,
[Name] VARCHAR(MAX) NOT NULL,
[SomeValue] DECIMAL(16, 5) NULL
)
SQL I wrote to copy data
DECLARE #BatchSize INT
DECLARE #Count INT
​
-- Leave these the same --
SET #Count = 1
​
-- Update these to modify run behavior --
SET #BatchSize = 5000
​
WHILE #Count > 0
BEGIN
SET IDENTITY_INSERT [dbo].[NewTable] ON;
INSERT INTO [dbo].[NewTable]
([Id],
[ForeignKey1],
[ForeignKey2],
[ForeignKey3],
[Name],
[SomeValue])
SELECT TOP (#BatchSize)
[Id],
[ForeignKey1],
[ForeignKey2],
[ForeignKey3],
[Name],
[SomeValue]
FROM [dbo].[Table]
WHERE not exists(SELECT 1 FROM [dbo].[NewTable] WHERE [dbo].[NewTable].Id = [dbo].[Table].Id)
ORDER BY Id
​
SET #Count = ##ROWCOUNT
​
SET IDENTITY_INSERT [dbo].[NewTable] OFF;
END
The Problem
Somehow data is getting garbled or modified in a seemingly random pattern during the copy. Most (maybe all) of the modified data we've seen has been for the ForeignKey2 column. And the value we end up with in the new table is seemingly random as well as it didn't exist at all in the old table. There doesn't seem to be any rhyme or reason to which records it affects either.
For example, here is one row for the original table and the corresponding row in the new table:
Old Table
ID: 204663
FK1: 452
FK2: 522413
FK3: 11190
Name: Masked
Some Value: 0.0
New Table
ID: 204663
FK1: 452
FK2: 120848
FK3: 11190
Name: Masked but matches Old Table
Some Value: 0.0
Environment
SQL was run in SSMS. Database is an Azure SQL Database.

SQL Server trigger not acting as expected

I have these two tables:
CREATE TABLE [dbo].[Test_Table1]
(
[id] [int] IDENTITY(1,1) NOT NULL,
[f_id] [int] NULL
) ON [PRIMARY]
CREATE TABLE [dbo].[Test_Table2_Tbl]
(
[f_id] [int] IDENTITY(1,1) NOT NULL,
[text] [varchar](500) NULL
) ON [PRIMARY]
And a trigger:
CREATE TRIGGER [dbo].[trg_Test_Trigger_Delete]
ON [dbo].[Test_Table2_Tbl]
AFTER DELETE
AS
INSERT INTO Test_Table2_Tbl (text)
(SELECT id FROM deleted)
UPDATE Test_Table1_Tbl
SET f_id = NULL
WHERE f_id IN (SELECT id FROM deleted)
GO
The keen observer will realize that 'id' does not exist in 'deleted'.
SQL catches that error in the INSERT, but without the INSERT it will let you add the trigger without any complaints.
Why doesn't it catch that error? (Invalid column name 'id'.)
My second question is, why does the UPDATE statement update EVERY column to NULL when id is not found in deleted?
I'm just very confused and need so clarity on why every record matches that WHERE clause.
Your error in the UPDATE is because the query is treated as a correlated subquery:
UPDATE Test_Table1_Tbl
SET f_id = NULL
WHERE Test_Table1_Tbl.f_id IN (SELECT Test_Table1_Tbl.id FROM deleted d);
id doesn't resolve in deleted, so SQL looks out at the next level.
These are the scoping rules for subqueries. That is why you should always use qualified table names when you have subqueries:
UPDATE Test_Table1_Tbl
SET f_id = NULL
WHERE Test_Table1_Tbl.f_id IN (SELECT d.id FROM deleted d);

Is it possible to restrict a sql table to only have a single row at the design stage

I have a simple table as scripted below.
CREATE TABLE dbo.KeyNumbers (
RowId INT IDENTITY
,ProFormaNumber NCHAR(10) NULL
,SalesOrderNumber NCHAR(10) NULL
,PurchaseOrderNumber NCHAR(10) NULL
,InvoiceCreditNoteNumber NCHAR(10) NULL
,InvoiceNumber NCHAR(10) NULL
,PurchaseInvoiceCreditNoteNumber NCHAR(10) NULL
,ModifiedDate DATETIME NULL
,CONSTRAINT PK_KeyNumbers PRIMARY KEY CLUSTERED (RowId)
) ON [PRIMARY]
The table is used to store key document numbers (Invoice number, Sales Order number etc) for the company, and as such only requires a single row. The main interaction with this table is done through stored procedures so the end use should never need to access it, but I am wondering if there is a way in SQL server to actively restrict the table to have one, and only one row, and to be able to do that at the design stage.
EDIT
Proof that Gordon's suggestion works nicely
The obvious method uses a trigger on insert to be sure the table is empty.
I've never tried this, but an index on a computed column might also work:
alter table dbo.KeyNumbers add OneAndOnly as ('OneAndOnly');
alter table dbo.KeyNumbers add constraint unq_OneAndOnly unique (OneAndOnly);
This should generate a unique key violation if a second row is inserted.
--I think this would be cleaner and utilizes the existing columns
CREATE TABLE dbo.KeyNumbers (
RowId AS (1) PERSISTED
,ProFormaNumber NCHAR(10) NULL
,SalesOrderNumber NCHAR(10) NULL
,PurchaseOrderNumber NCHAR(10) NULL
,InvoiceCreditNoteNumber NCHAR(10) NULL
,InvoiceNumber NCHAR(10) NULL
,PurchaseInvoiceCreditNoteNumber NCHAR(10) NULL
,ModifiedDate DATETIME NULL
,CONSTRAINT PK_KeyNumbers PRIMARY KEY CLUSTERED (RowId)
) ON [PRIMARY]
You could also use instead of trigger to accomplish the same..
create table test
(
id int
)
create trigger dbo.test_trgr
on dbo.test
instead of insert
as
begin
--here we check table rows,if there are no rows,iinsert..else ignore
if not exists(select 1 from dbo.test)
begin
insert into dbo.test
select * from inserted
end
Define one of the columns (any column will do) with:
a check constraint, requiring it to be the value you ultimately want
a unique constraint
For example, if you want the id column to have value 1:
create table mytable (
id int check (id = 1) unique,
othercol1 text,
othercol2 int // etc
)
Now there can be at most 1 row and it must have id = 1.

Insert using stored procedure

I want to insert a record into multiple tables at single time using a stored procedure. But if it already exists, that record could not be inserted. How can it? I need help. I have link between the tables.
CREATE TABLE [dbo].[tblrole]
(
[roleid] [INT] IDENTITY(1, 1) NOT NULL,
[rolename] [VARCHAR](50) NULL,
PRIMARY KEY CLUSTERED ([roleid] ASC)
)
It's normal that you cannot insert a duplicate record if you have a unique primary key.
You have been talking about multiple tables, but you've schown us just one table definition.
I I've understood well your problem, you would something like this:
create proc insert_data
-- params are coming here
as
if not exists(select 1 from your_target_table1 where column = #condition)
-- your insert comes here
else
-- do nothing or log en error in an error table or do an update
if not exists(select 1 from your_target_table2 where column = #condition)
-- your insert comes here
else
-- do nothing or log en error in an error table or do an update
-- and soon

Supposedly easy trigger

I've created a trigger which is to block inserted records with a date already existing in a table.
CREATE TRIGGER [dbo].[SpecialOffers_Insert]
ON [dbo].[SpecialOffers]
FOR INSERT,UPDATE
AS
SET NOCOUNT ON
IF EXISTS (SELECT * FROM inserted WHERE SPO_DateFrom IN (SELECT SPO_DateFrom FROM dbo.SpecialOffers))
BEGIN
RAISERROR('Error. ', 16, 1)
ROLLBACK TRAN
SET NOCOUNT OFF
RETURN
END
SET NOCOUNT OFF
It is added to a table:
CREATE TABLE [dbo].[SpecialOffers](
[SPO_SpoId] [int] IDENTITY(1,1) NOT NULL,
[SPO_DateFrom] [datetime] NOT NULL,
[SPO_DateTo] [datetime] NOT NULL)
The table is empty but when trying to insert such record:
INSERT INTO dbo.SpecialOffers (SPO_DateFrom, SPO_DateTo) VALUES ('2015-01-15','2015-01-15')
I got the Error from the trigger. How should I modify the trigger not to get the error?
If the goal is to block inserted records with date already existing in a table, you don't need a trigger - just create a unique constraint on the date field:
ALTER TABLE [dbo].[SpecialOffers]
ADD CONSTRAINT SpecialOffersUQ UNIQUE (SPO_DateFrom)
If you wanted a trigger to prevent overlaps, why didn't you say so:
CREATE TABLE [dbo].[SpecialOffers](
[SPO_SpoId] [int] IDENTITY(1,1) NOT NULL,
[SPO_DateFrom] [datetime] NOT NULL,
[SPO_DateTo] [datetime] NOT NULL,
constraint CK_SO_NoTimeTravel CHECK (SPO_DateFrom <= SPO_DateTo)
)
GO
CREATE TRIGGER NoOverlaps
on dbo.SpecialOffers
after insert,update
as
set nocount on
if exists (
select *
from dbo.SpecialOffers so1
inner join
dbo.SpecialOffers so2
on
so1.SPO_DateFrom < so2.SPO_DateTo and
so2.SPO_DateFrom < so1.SPO_DateTo and
so1.SPO_SpoId != so2.SPO_SpoId
inner join
inserted i
on
so1.SPO_SpoId = i.SPO_SpoId
)
begin
RAISERROR('No overlaps',16,1)
ROLLBACK
end
Examples:
--Works
INSERT INTO SpecialOffers (SPO_DateFrom,SPO_DateTo)
values ('20010101','20011231')
GO
--Fails (Trigger)
INSERT INTO SpecialOffers (SPO_DateFrom,SPO_DateTo)
values ('20010101','20011231')
GO
--Fails (Constraint)
INSERT INTO SpecialOffers (SPO_DateFrom,SPO_DateTo)
values ('20011231','20010101')
GO
--Fails (Trigger)
INSERT INTO SpecialOffers (SPO_DateFrom,SPO_DateTo)
values ('20020101','20021231'),
('20020701','20030630')
I also added a check constraint so that I didn't have to deal with nonsense data in the trigger.
You might have to change swap some of the <s for <=s or vice-versa, depending on what definition of intervals you want to use (i.e. are DateFrom and DateTo meant to be inclusive or exclusive endpoints for the interval they're describing?)
Since the trigger runs in the transaction context of the SQL statement that fired it, after this INSERT, there will be a row in your table dbo.SpecialOffers with the SPO_DateFrom values you've just inserted and the SELECT from the table will succeed ...
Therefore, the trigger will assume that there's already been a value - and it throws the error (as designed).
You could rewrite the trigger to not look at the newly inserted rows, but anything else - but as others have pointed out, a UNIQUE constraint does that much more simply
You should check if the rows you found are actually NOT the ones you have just inserted. Change the line
IF EXISTS (
SELECT * FROM inserted
WHERE SPO_DateFrom IN (
SELECT SPO_DateFrom
FROM dbo.SpecialOffers)
)
To
IF EXISTS (
SELECT * FROM inserted a
WHERE SPO_DateFrom IN (
SELECT SPO_DateFrom
FROM dbo.SpecialOffers b
WHERE a.SPO_SpoId <> b.SPO_SpoId)
)