SQL Server trigger(s) to maintain one IsPrimary/IsDefault row per FK - sql-server

We have a few tables with an IsPrimary column, e.g. many members belonging to an account. The requirement is if an account has one or more members, one and only one of them must have IsPrimary = 1. We want to achieve this using triggers for both maximum assurance of data integrity and ease of use from applications. But due to the batch nature of triggers, I'm struggling to accomplish it and in the most efficient way.
So far I have an insert/delete trigger (see below) that handles inserting a new primary record or deleting the primary record. Where I got stuck is ensuring the first record inserted has IsPrimary=1 and then realizing there could be multiple modifications to the same account in the batch...
Does anyone have any experience or and example with something like this?
ALTER TRIGGER dbo.trg_PrimaryTest_InsertDelete
ON dbo.PrimaryTest
AFTER INSERT,DELETE
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
--SET NOCOUNT ON;
PRINT 'executing trigger'
--If inserting a primary, set all others to 0
UPDATE PrimaryTest
SET IsPrimary = 0
FROM inserted
INNER JOIN PrimaryTest ON inserted.fk_ID = PrimaryTest.fk_ID
WHERE inserted.IsPrimary = 1
AND PrimaryTest.pk_ID <> inserted.pk_ID
AND PrimaryTest.IsPrimary = 1
--If deleting the primary, set most recent remaining phone to 1
UPDATE PrimaryTest
SET IsPrimary = 1
WHERE PrimaryTest.pk_ID IN (
SELECT TOP 1 PrimaryTest.pk_ID
FROM deleted
INNER JOIN PrimaryTest ON deleted.fk_ID = PrimaryTest.fk_ID
WHERE deleted.IsPrimary = 1
ORDER BY PrimaryTest.CreatedDate DESC
)
PRINT 'trigger executed'
END
GO
Table ddl:
CREATE TABLE [dbo].[PrimaryTest](
[pk_ID] [uniqueidentifier] NOT NULL,
[Value] [nvarchar](50) NULL,
[CreatedDate] [datetime2](7) NOT NULL,
[IsPrimary] [bit] NOT NULL,
[fk_ID] [int] NOT NULL,
CONSTRAINT [PK_PrimaryTest] PRIMARY KEY CLUSTERED
(
[pk_ID] ASC
)
GO
EDIT:
I think this might work or is at least headed in the right direction. I think it may be reading more records than it needs to in the 2nd C.T.E. case though. (Note I added update)
ALTER TRIGGER dbo.trg_PrimaryTest_InsertDelete
ON dbo.PrimaryTest
AFTER INSERT, UPDATE, DELETE
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
--SET NOCOUNT ON;
PRINT 'executing trigger'
--If setting a new primary, set all others to 0
UPDATE PrimaryTest
SET IsPrimary = 0
FROM inserted
INNER JOIN PrimaryTest ON inserted.fk_ID = PrimaryTest.fk_ID
WHERE inserted.IsPrimary = 1
AND PrimaryTest.pk_ID <> inserted.pk_ID
AND PrimaryTest.IsPrimary = 1
--Set IsPrimary on any modified sets left without an primary
;WITH cte
AS
(
SELECT *, ROW_NUMBER() OVER(PARTITION BY fk_ID ORDER BY CreatedDate DESC) as RowNum
FROM PrimaryTest p1
WHERE fk_ID IN (SELECT FK_ID FROM inserted UNION SELECT FK_ID FROM deleted) --Only look at modified sets
AND NOT EXISTS (SELECT NULL FROM PrimaryTest p2 WHERE p2.fk_ID = p1.fk_ID AND p2.IsPrimary = 1) --Select rows in a set without an IsPrimary=1 record
)
UPDATE cte
SET IsPrimary = 1
WHERE RowNum = 1
PRINT 'trigger executed'
END
GO

Related

SQL Server : prevent change of field value when it was once != NULL

I want to create a trigger in SQL Server that prevents an update on a row if one specific field in that row already contains NON-NULL values.
It should then just ROLLBACK the update.
Background: if a onceChangedDate is set, it shall not be able to change it or NULL it again.
Table structure:
ID, UserName, Hidden, ChangedOneDate
Each entry will have normally at creation:
ID, SomeUser, 0, NULL
I have a trigger which will set ChangedOnceDate to the current date as soon the "Hidden" is set to 1.
And then I want to prevent any change on ChangedOnceDate for future.
How can I achieve this?
This is some what of a stab in the dark, but seems like an EXISTS where the value of the column in the deleted pseudo-table isn't NULL but is in the inserted pseudo-table is what you are after:
--Sample Table
CREATE TABLE dbo.YourTable (ID int IDENTITY(1,1),
SomeColumn varchar(10) NOT NULL,
NullableDate date NULL,
CONSTRAINT PK_YourTable PRIMARY KEY (ID));
GO
--Trigger solution
CREATE TRIGGER dbo.trg_NullableDateNulled_YourTable ON dbo.YourTable
AFTER UPDATE AS
BEGIN
IF EXISTS (SELECT 1
FROM inserted i
JOIN deleted d ON i.ID = d.ID
WHERE d.NullableDate IS NOT NULL
AND i.NullableDate IS NULL)
--Use an Error number relevant for your environment
THROW 78921,
N'A row where the column ''NullableDate'' has been set to NULL has been detected in the trigger ''trg_NullableDateNulled_YourTable''. Cannot update column ''NullableDate'' to be NULL when it previously had a non-NULL value in the object ''dbo.YourTable''.',
16;
END;
GO
You can then test (and clean up) with the following:
INSERT INTO dbo.YourTable (SomeColumn, NullableDate)
VALUES('asda',NULL),
('wera',GETDATE());
GO
--Following fails
UPDATE dbo.YourTable
SET NullableDate = NULL
WHERE SomeColumn = 'wera';
GO
--Following works
UPDATE dbo.YourTable
SET NullableDate = GETDATE()
WHERE SomeColumn = 'asda';
GO
--Following works
UPDATE dbo.YourTable
SET NullableDate = '20220317'
WHERE SomeColumn = 'wera';
GO
--Following fails (as now not NULL
UPDATE dbo.YourTable
SET NullableDate = NULL
WHERE SomeColumn = 'asda';
GO
SELECT *
FROM dbo.YourTable;
GO
DROP TABLE dbo.YourTable;
db<>fiddle
Change the part below from #Larnu 's answer,
IF EXISTS (SELECT 1 FROM inserted i
JOIN deleted d ON i.ID = d.ID
WHERE d.NullableDate IS NOT NULL
AND i.NullableDate IS NULL)
to below
IF EXISTS (SELECT 1 FROM deleted d WHERE d.NullableDate IS NOT NULL)
to get the exact behavior you want.
dbfiddle

How can I update the rows that are existed before the insert using Trigger ( SQL Server )?

I'm looking for a method to update old rows before an insert or update using a Trigger ,
For example , I have this table
ID PersonID Name Status
1 001 Alex False
2 002 Mark True
What I need exactly is that when I insert in this table a new row (3,003,Jane,True) , the column status should be affected to False ( all old rows ) only the new row will have True
So the expected result when applying the trigger will be like this :
ID PersonID Name Status
1 001 Alex False
2 002 Mark False
3 003 Jane True
How can I do this?
What I have tried:
ALTER TRIGGER [dbo].[dbo.TR_SetStatus] ON [dbo].[Person]
after INSERT
NOT FOR REPLICATION
AS
DECLARE #CursorTestID INT = 1;
DECLARE #RowCnt BIGINT = 0;
BEGIN
DECLARE #count INT;
SELECT #RowCnt = COUNT(*) FROM Person;
WHILE #CursorTestID <= #RowCnt
BEGIN
update Person set status=0
SET #CursorTestID = #CursorTestID + 1
END
END
I have two questions:
How can I update the rows that are existed before the insert using Trigger ( SQL Server )?
How can I pass a parameter to a trigger? (as an example PersonID)
DROP TABLE IF EXISTS dbo.Test;
CREATE TABLE dbo.Test
(
id tinyint identity(1,1)not null primary key,
person_id char(3)not null,
name varchar(50)not null,
status varchar(5) not null
)
insert dbo.Test(person_id,name,status)
values('001','alex','false'),('002','mark','true');
go
SELECT *FROM DBO.Test
DROP TRIGGER IF EXISTS dbo.II_Test;
GO
CREATE TRIGGER dbo.II_Test
ON dbo.Test
INSTEAD OF INSERT
AS
BEGIN
UPDATE DBO.Test SET status='FALSE';
INSERT DBO.Test(person_id,name,status)
SELECT I.person_id,I.name,I.status
FROM inserted AS I
END
GO
insert dbo.Test(person_id,name,status)
values('003','JANE','true');
select * from dbo.Test
Could you please check if the above is suitable for you
How can I update the rows that are existed before the insert using Trigger ( SQL Server )
For example, you can use INSTEAD OF-trigger
How can I pass a parameter to a trigger? (as an example PersonID)
This is not supported at all. If you need parameters the better way, I guess, is to use stored procedure
Finally, I solved my problem ( the first question ) :
ALTER TRIGGER [dbo].[dbo.TR_SetActive] ON [dbo].[test]
after INSERT
NOT FOR REPLICATION
AS
BEGIN
update dbo.test set status=0 WHERE Id < (SELECT MAX(Id) FROM dbo.test)
END
For the second question , I have used to get the last record as parameter:
set #PersonId = (select PersonId from inserted)
Can be simplfy with :
CREATE TRIGGER dbo.E_I_Test
ON dbo.Test
FOR INSERT
AS
BEGIN
UPDATE T
SET status = CASE WHEN I.id IS NULL THEN 0 ELSE 1 END
FROM dbo.Test as T
LEFT OUTER JOIN inserted AS I
ON T.id = I.id;
BEWARE.... status is a reserved Transact SQL keyword. Should not be use for any SQL identifier (table, name, column neme...)
Corrections made...

How can I update a column in SQL Server using a trigger

I am trying to create a trigger within SQL Server Management Studio that will increment a column value by 1 when a separate column has been updated within the same table.
The value for the column we want to update when the update script has been ran becomes NULL
My example is that I when I change the address of a customer, I want a column that goes up by 1 every time the address is changed i.e NoOfAddressess = 1, 2, 3 etc...
Here is the SQL code that I am writing
ALTER TRIGGER trg_customeraudit
ON tblCustomer
AFTER UPDATE, DELETE, INSERT
AS
INSERT INTO dbo.CustomerDetailsAudit
VALUES (CURRENT_USER, CURRENT_TIMESTAMP,
(SELECT CustomerID FROM inserted),
(SELECT CustomerAddress FROM deleted),
(SELECT CustomerAddress FROM inserted),
(SELECT CustomerPostcode FROM deleted),
(SELECT CustomerPostcode FROM inserted),
(SELECT NumberOfChangedAddresses FROM dbo.CustomerDetailsAudit)
)
IF ((SELECT CustomerAddress FROM inserted) =
(SELECT CustomerAddress FROM deleted) OR
(SELECT CustomerPostcode FROM deleted) =
(SELECT CustomerPostcode FROM inserted))
BEGIN
RAISERROR ('You must enter both a new postcode and address',16,10)
ROLLBACK TRANSACTION
END
ELSE
BEGIN
PRINT 'Transaction successful'
WHERE CustomerID = (SELECT CustomerID from inserted)
END
IF UPDATE (CustomerName)
BEGIN
RAISERROR ('You cannot change the customer name', 16, 10)
ROLLBACK
END
Depending on the other things happening on this data triggers can be a very inefficient method of handling this, but here is one possible solution.
1. Setup
First create a table to use for testing.
create table test_table (
MyPrimaryKey int primary key clustered not null identity(1, 1)
, SomeColumn varchar(255) not null
, SomeColumnCounter int null
);
go
Now, add a trigger to initialize the counter to 1. This could be handled by a default constraint or set at the application level but it can also be done with a trigger.
-- this trigger will set the counter to 1 when a record is first added
-- doesn't need to be a trigger, but since the question was on triggers
create trigger trg_test_table_insert
on test_table
after insert
as
update tt
set tt.SomeColumnCounter = 1
from
test_table as tt
inner join
Inserted as i
on
tt.MyPrimaryKey = i.MyPrimaryKey;
go
Now, add a trigger that will check for changes on the designated column and increment the counter if needed.
-- this trigger will increment the counter by 1 if 'SomeColumn' changed
-- doesn't handle nulls so will need to be modified depending on schema
create trigger trg_test_table_update
on test_table
after update
as
update tt
set tt.SomeColumnCounter = tt.SomeColumnCounter + 1
from
Inserted as i -- new version of the record
inner join
Deleted as d -- old version of the record
on
i.MyPrimaryKey = d.MyPrimaryKey
and i.SomeColumn <> d.SomeColumn
inner join
test_table as tt
on
tt.MyPrimaryKey = i.MyPrimaryKey
go
2. Testing
Add some test data.
insert into test_table (SomeColumn)
values ('abc'), ('def');
go
Now we have:
MyPrimaryKey SomeColumn SomeColumnCounter
1 abc 1
2 def 1
Update without changing anything:
update tt
set tt.SomeColumn = 'abc'
from
test_table as tt
where
tt.MyPrimaryKey = 1
We still have:
MyPrimaryKey SomeColumn SomeColumnCounter
1 abc 1
2 def 1
Update that actually changes something:
update tt
set tt.SomeColumn = 'abbc'
from
test_table as tt
where
tt.MyPrimaryKey = 1
Now we have:
MyPrimaryKey SomeColumn SomeColumnCounter
1 abbc 2
2 def 1
Update that changes everything:
update tt
set tt.SomeColumn = tt.SomeColumn + 'z'
from
test_table as tt
Now we have:
MyPrimaryKey SomeColumn SomeColumnCounter
1 abbcz 3
2 defz 2

SQL Server trigger on Insert and Update

I am looking to create a SQL Server trigger that moves a record from one table to an identical replica table if the record matches a specific condition.
Questions: do I need to specify each column, or can I use a wildcard?
Can I use something like:
SET #RecID = (SELECT [RecoID] FROM Inserted)
IF NULLIF(#RecID, '') IS NOT NULL
(then insert....)
THANKS!
There's a lot of stuff you "CAN" do in a trigger, but that doesn't mean you should. I'd would urge to to avoid setting scalar variables within a trigger at all costs. Even if you 100% sure your table will never have more that 1 row inserted per transaction because that's how the app is designed... You'll be in for very rude awakening when you find out that not all transactions come through the application.
Below is a quick demonstration of both types of triggers...
USE tempdb;
GO
IF OBJECT_ID('tempdb.dbo.PrimaryTable', 'U') IS NOT NULL
DROP TABLE dbo.PrimaryTable;
GO
IF OBJECT_ID('tempdb.dbo.TriggerScalarLog', 'U') IS NOT NULL
DROP TABLE dbo.TriggerScalarLog;
GO
IF OBJECT_ID('tempdb.dbo.TriggerMultiRowLog', 'U') IS NOT NULL
DROP TABLE dbo.TriggerMultiRowLog;
GO
CREATE TABLE dbo.PrimaryTable (
Pt_ID INT NOT NULL IDENTITY (1,1) PRIMARY KEY CLUSTERED,
Col_1 INT NULL,
Col_2 DATE NOT NULL
CONSTRAINT df_Col2 DEFAULT (GETDATE())
);
GO
CREATE TABLE dbo.TriggerScalarLog (
Pt_ID INT,
Col1_Old INT,
Col1_New INT,
Col2_Old DATE,
Col2_New DATE
);
GO
CREATE TABLE dbo.TriggerMultiRowLog (
Pt_ID INT,
Col1_Old INT,
Col1_New INT,
Col2_Old DATE,
Col2_New DATE
);
GO
--=======================================================
CREATE TRIGGER dbo.PrimaryCrudScalar ON dbo.PrimaryTable
AFTER INSERT, UPDATE, DELETE
AS
SET NOCOUNT ON;
DECLARE
#Pt_ID INT,
#Col1_Old INT,
#Col1_New INT,
#Col2_Old DATE,
#Col2_New DATE;
SELECT
#Pt_ID = ISNULL(i.Pt_ID, d.Pt_ID),
#Col1_Old = d.Col_1,
#Col1_New = i.Col_1,
#Col2_Old = d.Col_2,
#Col2_New = i.Col_2
FROM
Inserted i
FULL JOIN Deleted d
ON i.Pt_ID = d.Pt_ID;
INSERT dbo.TriggerScalarLog (Pt_ID, Col1_Old, Col1_New, Col2_Old, Col2_New)
VALUES (#Pt_ID, #Col1_Old, #Col1_New, #Col2_Old, #Col2_New);
GO -- DROP TRIGGER dbo.PrimaryCrudScalar;
CREATE TRIGGER PrimaryCrudMultiRow ON dbo.PrimaryTable
AFTER INSERT, UPDATE, DELETE
AS
SET NOCOUNT ON;
INSERT dbo.TriggerMultiRowLog (Pt_ID, Col1_Old, Col1_New, Col2_Old, Col2_New)
SELECT
ISNULL(i.Pt_ID, d.Pt_ID),
d.Col_1,
i.Col_1,
d.Col_2,
i.Col_2
FROM
Inserted i
FULL JOIN Deleted d
ON i.Pt_ID = d.Pt_ID;
GO -- DROP TRIGGER dbo.TriggerMultiRowLog;
--=======================================================
--=======================================================
-- --insert test...
INSERT dbo.PrimaryTable (Col_1)
SELECT TOP 100
o.object_id
FROM
sys.objects o;
SELECT 'INSERT Scarar results';
SELECT * FROM dbo.TriggerScalarLog tsl;
SELECT 'INSERT Multi-Row results';
SELECT * FROM dbo.TriggerMultiRowLog tmrl;
UPDATE pt SET
pt.Col_1 = pt.Col_1 + rv.RandomVal,
pt.Col_2 = DATEADD(DAY, rv.RandomVal, pt.Col_2)
FROM
dbo.PrimaryTable pt
CROSS APPLY ( VALUES (ABS(CHECKSUM(NEWID())) % 10000 + 1) ) rv (RandomVal);
SELECT 'UPDATE Scarar results';
SELECT * FROM dbo.TriggerScalarLog tsl;
SELECT 'UPDATE Multi-Row results';
SELECT * FROM dbo.TriggerMultiRowLog tmrl;
DELETE pt
FROM
dbo.PrimaryTable pt;
SELECT 'DELETE Scarar results';
SELECT * FROM dbo.TriggerScalarLog tsl;
SELECT 'DELETE Multi-Row results';
SELECT * FROM dbo.TriggerMultiRowLog tmrl;
You could, but I'd recommend against it. If your source table changed things would start failing.
Also, in your example if you were to ever have more than one row inserted at a time you would get thrown an error (or have unpredictable results). I'd recommend a more set based approach:
INSERT table2 ( user_id ,
user_name ,
RecoID
)
SELECT user_id ,
user_name ,
RecoID
FROM inserted i
LEFT JOIN table2 t ON i.RecoID = t.RecoID
WHERE t.RecoID IS NULL;
EDIT:
If you want to stop the insert happening on your original table then you'll need to do something along the lines of:
CREATE TRIGGER trigger_name
ON table_orig
INSTEAD OF INSERT
AS
BEGIN
-- make sure we aren't triggering from ourselves from another trigger
IF TRIGGER_NESTLEVEL() <= 1
return;
-- insert into the table_copy if the inserted row is already in table_orig (not null)
INSERT table_copy ( user_id ,
user_name ,
RecoID
)
SELECT user_id ,
user_name ,
RecoID
FROM inserted i
LEFT JOIN table_orig c ON i.RecoID = c.RecoID
WHERE t.RecoID IS NOT NULL;
-- insert into table_orig if the inserted row is not already in table_orig (null)
INSERT table_orig ( user_id ,
user_name ,
RecoID
)
SELECT user_id ,
user_name ,
RecoID
FROM inserted i
LEFT JOIN table_orig c ON i.RecoID = c.RecoID
WHERE t.RecoID IS NULL;
END;
The instead of will stop the insert if you don't want it to actually be inserted, so you'll need to do that yourself (the second insert statement).
Please note I changed some nulls to not nulls and the table we are left joining to in some cases.

SQL Server - alternative to using NOT EXISTS

I have a list of about 200,000 records with EntityID column which I load into a temp table variable.
I want to insert any records from the Temp table variable if EntityID from the Temp table does not exist in the dbo.EntityRows table. The dbo.EntityRows table contains about 800,000 records.
The process is very slow compared to when the dbo.EntityRows table had about 500,000 records.
My first guess is because of the NOT EXISTS clause, each row from the Temp variable must scan the entire 800k rows of the dbo.EntityRows table to determine if it exists or not.
QUESTION: Are there alternative ways to run this comparison check without using the NOT EXISTS, which incurs a hefty cost and will only get worse as dbo.EntityRows continues to grow?
EDIT: Appreciate the comments. Here is the query (I left out the part after the IF NOT EXISTS check. After that, if NOT EXISTS, I insert into 4 tables).
declare #EntityCount int, #Counter int, #ExistsCounter int, #AddedCounter int
declare #LogID int
declare #YdataInsertedEntityID int, #YdataSearchParametersID int
declare #CurrentEntityID int
declare #CurrentName nvarchar(80)
declare #CurrentSearchParametersID int, #CurrentSearchParametersIDAlreadyDone int
declare #Entities table
(
Id int identity,
EntityID int,
NameID nvarchar(80),
SearchParametersID int
)
insert into #Entities
select EntityID, NameID, SearchParametersID from YdataArvixe.dbo.Entity order by entityid;
set #EntityCount = (select count(*) from #Entities);
set #Counter = 1;
set #LogID = null;
set #ExistsCounter = 0;
set #AddedCounter = 0;
set #CurrentSearchParametersIDAlreadyDone = -1;
While (#EntityCount >= #Counter)
begin
set #CurrentEntityID = (select EntityID from #Entities
where id = #Counter)
set #CurrentName = (select nameid from #Entities
where id = #Counter);
set #CurrentSearchParametersID = (select SearchParametersID from #Entities
where id = #Counter)
if not exists (select 1 from ydata.dbo.entity
where NameID = #CurrentName)
begin
-- I insert into 4 tables IF NOT EXISTS = true
end
I am not sure but there are following ways how we can check
(SELECT COUNT(er.EntityID) FROM dbo.EntityRows er WHERE er.EntityID = EntityID) <> 0
(SELECT er.EntityID FROM dbo.EntityRows er WHERE er.EntityID = EntityID) IS NOT NULL
EntityID NOT EXISTS (SELECT er.EntityID FROM dbo.EntityRows er)
EntityID NOT IN (SELECT er.EntityID FROM dbo.EntityRows er)
But as per my belief getting count will give good performance.
Also index will help to improve performance as 'Felix Pamittan' said
As #gotqn said, start by using a temporary table. Create an index on EntityID after the table is filled. If you don't have an index on EntityID in EntityRows, create one.
I do things like this a lot, and I generally use the following pattern:
INSERT INTO EntityRows (
EntityId, ...
)
SELECT T.EntityId, ...
FROM #tempTable T
LEFT JOIN EntityRows E
ON T.EntityID = E.EntityID
WHERE E.EntityID IS NULL
Please comment if you'd like further info.
Well, the answer was pretty basic. #Felix and #TT had the right suggestion. Thanks!
I put a non-clustered index on the NameID field in ydata.dbo.entity.
if not exists (select 1 from ydata.dbo.entity
where NameID = #CurrentName)
So it can now process the NOT EXISTS part quickly using the index instead of scanning the entire dbo.entity table. It is moving fast again.

Resources