Complex multi-column unique constraint - sql-server

I have a table with 10 columns but only care about 3 columns for this. Imagine my table looks like this:
CREATE TABLE MyTable ( RowID int IDENTITY(1,1), UserID int, NodeID int, RoleID int )
What I need is a constraint that enforces the following: UserID and RoleID need to be unique for each NodeID (i.e. a user cannot have the same role in multiple nodes). In other words I want to allow
INSERT MyTable (UserID, NodeID, RoleID) SELECT 1, 1, 1
but not allow
INSERT MyTable (UserID, NodeID, RoleID) SELECT 1, 2, 1
if the first insert has occurred because that would result in a user having a role in multiple nodes.
Hopefully this is simple and I'm just making it more complex than it needs to be in my brain.

Since your constraint depends on data in other rows, this rules out a filtered index. IMO a viable option could be a trigger. Such a trigger could look like something like this:
CREATE TRIGGER dbo.MyTrigger ON dbo.Q1
AFTER INSERT, UPDATE
AS
DECLARE #userId INT, #Id INT, #roleId INT, #exists INT;
SELECT TOP 1
#userId = userID
,#roleId = roleID
,#Id = Id
FROM inserted;
SELECT TOP 1
#exists = Id
FROM Q1
WHERE userId = #userId
AND roleID = #roleID AND Id<> #Id;
IF ISNULL(#exists, 0) > 0
BEGIN
-- you would want to either undo the action here when you use an 'after' trigger
-- because as the name implies ... the after means the record is allready inserted/updated
RAISERROR ('No way we would allow this.', 16, 1);
END
-- else
-- begin
-- another alternative would be to use a instead of trigger, which means the record
-- has not been inserted or updated and since that type of trigger runs the trigger 'instead of'
-- updating or inserting the record you would need to do that yourself. Pick your poison ...
-- end
GO

An unique index should enforce your requirements
CREATE UNIQUE NONCLUSTERED INDEX [idx_Unique] ON [dbo].[MyTable]
(
[UserID] ASC,
[NodeID] ASC,
[RoleID] ASC
)
From the comments I suppose you will need two unique indices
CREATE UNIQUE NONCLUSTERED INDEX [idx_User_Node] ON [dbo].[MyTable]
(
[UserID] ASC,
[NodeID] ASC
)
GO
CREATE UNIQUE NONCLUSTERED INDEX [idx_User_Role] ON [dbo].[MyTable]
(
[UserID] ASC,
[RoleID] ASC
)

Related

CTE to remove duplicate data

I have a table where the rows are duplicated. I would like to remove the duplicates and add a Composite Key to avoid duplicates.
;WITH myCTE (RowNumber, invoice_id, Invoice_Number, Organization_id, status, created_at) AS
(
SELECT
ROW_NUMBER() OVER (PARTITION BY Invoice_Number ORDER BY invoice_id, Invoice_Number DESC) AS RowNumber ,
invoice_id, Invoice_Number, Organization_id,status, created_at
FROM
Invoice_Export
)
SELECT *
FROM myCTE
WHERE Invoice_number LIKE '%-00%'
ORDER BY invoice_id
select * from Invoice_Export
ALTER TABLE Invoice_Export ALTER COLUMN [Organization_id] NVARCHAR(36) NOT NULL
ALTER TABLE Invoice_Export ALTER COLUMN [Invoice_Number] NVARCHAR(15) NOT NULL
ALTER table Invoice_Export
ADD CONSTRAINT [Composite_Key_Invoice] PRIMARY KEY CLUSTERED (Organization_id, Invoice_Number)
Is there any other better approach for the same.
I'm guessing that 'invoice_id' is unique, so
DELETE FROM Invoice_Export duplicate
WHERE EXISTS (SELECT 1 FROM Invoice_Export
WHERE duplicate.invoice_id > Invoice_Export.invoice_id
AND duplicate.Organization_id = Invoice_Export.Organization_id
AND duplicate.Invoice_Number = Invoice_Export.Invoice_Number)
instead PRIMARY KEY you can use UNIQUE (after removing duplicates)
ALTER TABLE Invoice_Export
ADD CONSTRAINT UC_Invoice_Export UNIQUE (Organization_id, Invoice_Number);

Primary key of temp table already exists

I have the following query
declare #var1 int, #var2 int
set #var1 = 110
set #var2 = 300
IF object_id('tempdb..#tbl_Contract') IS NOT NULL
BEGIN
ALTER TABLE #tbl_Contract drop constraint PK_#tbl_Contract
DROP TABLE #tbl_Contract
END
CREATE TABLE #tbl_Contract
( ContractID int NOT NULL
, PersonID int NOT NULL
, EndDate smalldatetime NULL
, CONSTRAINT [PK_#tbl_Contract] PRIMARY KEY CLUSTERED
(
ContractID ASC
)
)
...
But when I run this query the second time I'm getting an error:
Msg 2714, Level 16, State 5, Line 1 There is already an object named
'PK_#tbl_Contract' in the database. Msg 1750, Level 16, State 0,
Line 1 Could not create constraint. See previous errors.
What I'm doing wrong? Why my primary key was not removed?
Should I use GO after ALTER? But I can't because there are some varibles
You don't need to drop a constraint and then immediately drop the table. It is simpler to just drop the table. Also, there really is no need to name constraints in a temp table. You can greatly simplify your code like this.
IF object_id('tempdb..#tbl_Contract') IS NOT NULL
BEGIN
DROP TABLE #tbl_Contract
END
GO
CREATE TABLE #tbl_Contract
( ContractID int NOT NULL PRIMARY KEY CLUSTERED
, PersonID int NOT NULL
, EndDate smalldatetime NULL
)
For a composite primary key constraint on a temporary table it is much better not to name the primary key constraint, otherwise you will run into trouble with concurrent sessions. In the following way you can also add any number of indexes or check constraints without naming them:
IF object_id('tempdb..#tbl_Contract') IS NOT NULL
BEGIN
DROP TABLE #tbl_Contract
END
GO
CREATE TABLE #tbl_Contract
( ContractID int NOT NULL
, PersonID int NOT NULL
, EndDate smalldatetime NULL
, PRIMARY KEY CLUSTERED (ContractID ASC, PersonID ASC)
, CHECK (ContractID > 10 AND EndDate > N'2019-12-31')
)
This allows you to avoid the pitfall of #mpag's solution in that you can further use the temp table in your stored procedure. In #mpag's solution you cannot use #tbl_Contract outside the #Q statement.
You cannot create a primary key with the same name in different sessions, even on a temp table. If you do not name the primary key and other constraints, they are automatically given a different unique name by the system for each session.
Otherwise you will have the following error when concurrently running the procedure from different sessions:
Error number: 1750, error procedure:
dbo.ProcedureName, error line: XXX, error message:
Could not create constraint or index. See previous errors.
I had the same issue as #NReilingh raised in the comments (i.e. needing a composite primary key). So, lets assume we want a primary key on the combination of ContractID and PersonID. The following works in that circumstance (assuming you have appropriate permissions for EXEC):
IF object_id('tempdb..#tbl_Contract') IS NOT NULL
DROP TABLE #tbl_Contract
GO
DECLARE #Q as nvarchar(MAX) = N'
CREATE TABLE #tbl_Contract (
ContractID int NOT NULL
, PersonID int NOT NULL
, EndDate smalldatetime NULL
, CONSTRAINT
[PK_#tbl_Contract' + CAST(##SPID as nvarchar(6)) + N']
PRIMARY KEY CLUSTERED (
ContractID ASC,
PersonID ASC
)
)'
EXEC (#Q)
This works because each connection at any one time on a given server should have a unique SPID value. SPID values are typically a double-digit number greater than 50; but are of type smallint so could be up to 5 digits long, potentially signed.
Remember to double up any single quotes in within your table's DDL (i.e. replace ' with '')

How to Declaring scalar table in sql?

How to declare one element table so it can be used in future queries?
DECLARE #Emp TABLE
(
ID BIGINT NULL,
CompanyID BIGINT NULL
)
INSERT INTO #EMP
SELECT ID,CompanyID FROM Emp WHERE PIN = 123
SELECT COMPANYID FROM COMPANY WHERE ID = #Emp.CompanyID
You can not. A table per definition contains a (possibly) unlimited number of elements. However, you can always do something like this:
DECLARE #CompanyID BIGINT
SET #CompanyID = (SELECT TOP 1 CompanyID FROM #Emp WHERE ...)
By the way, the following line is not correct, as the WHERE clause is incomplete.
SELECT COMPANYID FROM COMPANY WHERE #Emp.CompanyID
If you intention is to create a table variable that will only store a maximum of one row, you can do it like this:
DECLARE #Emp TABLE
(
ID BIGINT NULL,
CompanyID BIGINT NULL,
Lock char(1) not null default 'X' primary key check(Lock='X')
)
INSERT INTO #EMP (ID,CompanyID)
SELECT ID,CompanyID FROM Emp WHERE PIN = 123
Because the table's primary key is constrained to only one possible value, logically there can't be more than one row.

SQL - Trigger for auto-incrementing number of signed in people

I am having a little bit of trouble with making a trigger in my SQL. I have two tables:
This one
Create table [user]
(
[id_user] Integer Identity(1,1) NOT NULL,
[id_event] Integer NULL,
[name] Nvarchar(15) NOT NULL,
[lastname] Nvarchar(25) NOT NULL,
[email] Nvarchar(50) NOT NULL, UNIQUE ([email]),
[phone] Integer NULL, UNIQUE ([phone]),
[pass] Nvarchar(50) NOT NULL,
[nick] Nvarchar(20) NOT NULL, UNIQUE ([nick]),
Primary Key ([id_user])
)
go
and this one
Create table [event]
(
[id_event] Integer Identity(1,1) NOT NULL,
[id_creator] Integer NOT NULL,
[name] Nvarchar(50) NOT NULL,
[date] Datetime NOT NULL, UNIQUE ([date]),
[city] Nvarchar(50) NOT NULL,
[street] Nvarchar(50) NOT NULL,
[zip] Integer NOT NULL,
[building_number] Integer NOT NULL,
[n_signed_people] Integer Default 0 NOT NULL Constraint [n_signed_people] Check (n_signed_people <= 20),
Primary Key ([id_akce])
)
Now I need a trigger for when I insert a new user with and id_event, or update existing one with one, to take the id_event I inserted, look in the table of events and increment the n_signed_people in a line with a coresponding id_event, until it is 20. When it is 20, it should say that the event is full. I made something like this, it is working when I add a new user with id, but now I need it to stop at 20 and say its full and also I am not sure if it will work, when I'll try to update existing user, by adding an id_event (I assume it was NULL before update).
CREATE TRIGGER TR_userSigning
ON user
AFTER INSERT
AS
BEGIN
DECLARE #idevent int;
IF (SELECT id_event FROM Inserted) IS NOT NULL --if the id_event is not empty
BEGIN
SELECT #idevent=id_event FROM Inserted; --the inserted id_event will be save in a local variable
UPDATE event SET n_signed_people = n_signed_people+1 WHERE #idevent = id_event;
END
END
go
Good evening,
I did notice some issues with your schema. I want to list the fixes I made in order.
1 - Do not use reserved words. Both user and event are reserved.
2 - Name your constraints. You will be glad they are not some random word when you want to drop one.
3 - I added a foreign key to make sure there is integrity in the relationship.
All this work was done in tempdb. Now, lets get to the fun stuff, the trigger.
-- Just playing around
use tempdb;
go
-- attendee table
if object_id('attendees') > 0
drop table attendees
go
create table attendees
(
id int identity (1,1) NOT NULL constraint pk_attendees primary key,
firstname nvarchar(15) NOT NULL,
lastname nvarchar(25) NOT NULL,
email nvarchar(50) NOT NULL constraint uc_email unique,
phone int NULL constraint uc_phone unique,
pass nvarchar(50) NOT NULL,
nick nvarchar(20) NOT NULL constraint uc_nick unique,
event_id int NOT NULL
)
go
-- events table
if object_id('events') > 0
drop table events
go
create table events
(
id int identity (1,1) NOT NULL constraint pk_events primary key,
creator int NOT NULL,
name nvarchar(50) NOT NULL,
planed_date datetime NOT NULL constraint uc_planed_date unique,
street nvarchar(50) NOT NULL,
city nvarchar(50) NOT NULL,
zip nvarchar(9) NOT NULL,
building_num int NOT NULL,
registered int
constraint df_registered default (0) NOT NULL
constraint chk_registered check (registered <= 20),
);
go
-- add some data
insert into events (creator, name, planed_date, street, city, zip, building_num)
values (1, 'new years eve', '20131231 20:00:00', 'Promenade Street', 'Providence', '02908', 99);
-- make sure their is integrity
alter table attendees add constraint [fk_event_id]
foreign key (event_id) references events (id);
I usually add all three options (insert, update, & delete). You coded for insert in the example above. But you did not code for delete.
Also, both the inserted and deleted tables can contain multiple rows. For instance, if two attendees decide to drop out, you want to minus 2 from the table.
-- create the new trigger.
CREATE TRIGGER [dbo].[trg_attendees_cnt] on [dbo].[attendees]
FOR INSERT, UPDATE, DELETE
AS
BEGIN
-- declare local variable
DECLARE #MYMSG VARCHAR(250);
-- nothing to do?
IF (##rowcount = 0) RETURN;
-- do not count rows
SET NOCOUNT ON;
-- deleted data
IF NOT EXISTS (SELECT * FROM inserted)
BEGIN
UPDATE e
SET e.registered = e.registered - c.total
FROM
[dbo].[events] e
INNER JOIN
(SELECT [event_id], count(*) as total
FROM deleted group by [event_id]) c
ON e.id = c.event_id;
RETURN;
END
-- inserted data
ELSE IF NOT EXISTS (SELECT * FROM deleted)
BEGIN
UPDATE e
SET e.registered = e.registered + c.total
FROM
[dbo].[events] e
INNER JOIN
(SELECT [event_id], count(*) as total
FROM inserted group by [event_id]) c
ON e.id = c.event_id;
RETURN;
END;
-- updated data (no counting involved)
END
GO
Like any good programmer, I need to test my work to make sure it is sound.
Lets add 21 new attendees. The check constraint should fire. This only works since the error generated by the UPDATE rollback the insert.
-- Add 21 attendees
declare #var_cnt int = 0;
declare #var_num char(2);
while (#var_cnt < 22)
begin
set #var_num = str(#var_cnt, 2, 0);
insert into attendees (firstname, lastname, email, phone, pass, nick, event_id)
values ('first-' + #var_num,
'last-' + #var_num,
'email-'+ #var_num,
5554400 + (#var_cnt),
'pass-' + #var_num,
'nick-' + #var_num, 1);
set #var_cnt = #var_cnt + 1
end
go
Last but not least, we need to test a DELETE action.
-- Delete the last row
delete from [dbo].[attendees] where id = 20;
go

Update timestamp column when Foreign table updates without Trigger

I'm trying to setup a Timestamp/Rowversion on a Parent table so that when the Child table updates, the Timestamp on the Parent table row changes.
I don't need to know about the row in the Child table, just that according to the parent this particular row has changed.
USE [Test]
GO
CREATE TABLE [dbo].[Clerk](
[ID] [int] NOT NULL,
[Name] [varchar](20) NOT NULL,
[lastUpdate] [timestamp] NOT NULL,
CONSTRAINT [PK_Clerk] PRIMARY KEY CLUSTERED
(
[ID] ASC
)
CREATE TABLE [dbo].[ClerkAddress](
[ClerkID] [int] NOT NULL,
[Address] [varchar](40) NOT NULL,
CONSTRAINT [PK_ClerkAddress] PRIMARY KEY CLUSTERED
(
[ClerkID] ASC
)
ALTER TABLE [dbo].[ClerkAddress] WITH CHECK ADD CONSTRAINT [FK_ClerkAddress_Clerk] FOREIGN KEY([ClerkID])
REFERENCES [dbo].[Clerk] ([ID])
ON UPDATE CASCADE
insert into Clerk (ID, Name) values (1, 'Test1')
insert into Clerk (id, Name) values (2, 'Test2')
insert into ClerkAddress (ClerkID, Address) values (1, 'address1')
insert into ClerkAddress (ClerkID, Address) values (2, 'address2')
using the following code examples.
update ClerkAddress set Address = NEWID() where ClerkID = 2
--no change to Clerk when address changes
select * from Clerk
select * from ClerkAddress
--Of course these update the lastUpdate in clerk
update Clerk set Name = 'test2' where ID = 2
update Clerk set Name = name
Is this even possible or do I need to make triggers for the updates? (update clerk set name = name where id = ClerkID)
You can make it appear as though the parent row is updated with a view.
You need to add a rowversion column to ClerkAddress, then
CREATE VIEW dbo.Clerk2
WITH SCHEMABINDING -- works for me on SQL Server 2012
AS
SELECT
C.ID, C.Name, ISNULL(MAX(CA.lastUpdate), C.lastUpdate) AS lastupdate
FROM
[dbo].[Clerk] C
LEFT JOIN
[dbo].[ClerkAddress] CA ON C.ID = CA.ClerkID
GROUP BY
C.ID, C.Name, C.lastUpdate
GO
SELECT * FROM dbo.Clerk2;
GO
update ClerkAddress set Address = NEWID() where ClerkID = 2;
GO
SELECT * FROM dbo.Clerk2;
GO
update ClerkAddress set Address = NEWID() where ClerkID = 2;
GO
SELECT * FROM dbo.Clerk2;
GO
This uses the highest value from rowversion yet preserves the actual rowversion on Clerk (which can still be used for optimistic concurrency by the client)
This works because rowversion is database unique

Resources