Update Trigger For Multiple Rows - sql

I am trying to Insert data in a table named "Candidate_Post_Info_Table_ChangeLogs" whenever a record is updated in another table named "Candidate_Personal_Info_Table". my code works fine whenever a single record is updated but when i try to updated multiple rows it gives error:
"Sub query returned more then 1 value".
Following is my code :
ALTER TRIGGER [dbo].[Candidate_PostInfo_UPDATE]
ON [dbo].[Candidate_Post_Info_Table]
AFTER UPDATE
AS
BEGIN
IF ##ROWCOUNT = 0
RETURN
DECLARE #Candidate_Post_ID int
DECLARE #Candidate_ID varchar(50)
DECLARE #Action VARCHAR(50)
DECLARE #OldValue VARCHAR(MAX)
DECLARE #NewValue VARCHAR(MAX)
DECLARE #Admin_id int
IF UPDATE(Verified)
BEGIN
SET #Action = 'Changed Verification Status'
SET #Candidate_Post_ID = (Select ID From inserted)
SET #Candidate_ID = (Select Identity_Number from inserted)
SET #NewValue = (Select Verified From inserted)
SET #OldValue = (Select Verified From deleted)
IF(#NewValue != #OldValue)
BEGIN
INSERT INTO Candidate_Post_Info_Table_ChangeLogs(Candidate_Post_ID, Candidate_ID, Change_DateTime, action, NewValue, OldValue, Admin_ID)
VALUES(#Candidate_Post_ID, #Candidate_ID, GETDATE(), #Action, #NewValue, #OldValue, '1')
END
END
END
i have searched stack overflow for this issue but couldn't get any related answer specific to this scenario.

When you insert/update multiple rows into a table, the Inserted temporary table used by the system holds all of the values from all of the rows that were inserted or updated.
Therefore, if you do an update to 6 rows, the Inserted table will also have 6 rows, and doing something like this:
SET #Candidate_Post_ID = (Select ID From inserted)
Will return an error, just the same as doing this:
SET #Candidate_Post_ID = (SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6)
From the looks of things, you tried to do this with an iterative approach. Set-based is better. Maybe consider doing it like this in the body of your TRIGGER (without all of the parameters...):
IF UPDATE(Verified)
BEGIN
INSERT INTO Candidate_Post_Info_Table_ChangeLogs
(
Candidate_Post_ID
,Candidate_ID
,Change_DateTime
,action
,NewValue
,OldValue
,Admin_ID
)
SELECT
I.ID
,I.Identity_Number
,GETDATE()
,'Changed Verification Status'
,I.Verified
,O.Verified
,'1'
FROM Inserted I
INNER JOIN Deleted O
ON I.ID = O.ID -- Check this condition to make sure it's a unique join per row
WHERE I.Verified <> O.Verified
END

A similar case was solved in the following thread using cursors.... please check it
SQL Server A trigger to work on multiple row inserts
Also the below thread gives the solution based on set based approach
SQL Server - Rewrite trigger to avoid cursor based approach
*Both the above threads are from stack overflow...

Related

Multiple Rows with in SQL INSTEAD of Trigger

looking a bit of direction or guidance as to why I’m getting multiple rows using my trigger. Basically I have a web app that controls Asset Types (i.e Laptops, Phones etc), what I’m trying to do with this trigger is when the Asset Type Name (at_typedesc) changes that I log to an audit table (in this case sql_log) what the old name was and what the new name is.
This is working, but for some reason I get multiple lines written at the INSERT TO SQL_LOG statement. It does write the old name & new name, but then I’ll get 3 additional rows which has the old name showing the new name...
This is currently on a 2008 SQL Server.
-- create the trigger
go
create trigger trg_InsteadOfUpdate on [dbo].[lkp_asset_types]
instead of update
as
begin
DECLARE #triggerAction varchar(1)
-- determine the TRIGGER action
-- this allows us to tell if its an INSERT or an UPDATE
SELECT #triggerAction = CASE
WHEN EXISTS(SELECT 1 FROM INSERTED)
AND EXISTS(SELECT 1 FROM deleted) THEN 'U'
WHEN EXISTS(SELECT 1 FROM inserted) THEN 'I'
ELSE 'D' END;
-- get the orginally asset name from the DELETED table
-- this contains the rows as they were BEFORE the UPDATE Statement
DECLARE #orgAssetTypeName varchar(255)
SET #orgAssetTypeName = (SELECT top 1 at_typedesc from lkp_asset_types WHERE at_id = (select at_id from deleted))
-- UPDATE to the new asset name based on the NEW value in the INSERTED Table
update lkp_asset_types
set at_typedesc = (select at_typedesc from inserted)
where at_id = (select at_id from inserted)
-- get the new asset name from the INSERTED table
-- this contains the rows as they were AFTER the UPDATE Statement
DECLARE #newAssetTypeName varchar(255)
SET #newAssetTypeName = (SELECT top 1 at_typedesc from lkp_asset_types WHERE at_id = (select at_id from inserted))
insert into sql_log
(sql_log)
values ('SQL PRE Changed from : ' + #orgAssetTypeName + ' to: ' + #newAssetTypeName + '. Action = ' + #triggerAction)
end
go
Logic like this in a trigger in SQL Server is just broken:
where at_id = (select at_id from inserted)
I really wish the SQL Server parser issued a warning when encountering such constructs.
There is no guarantee that inserted has only one value (nor deleted).
That is how SQL Server defines triggers: as set operations. If multiple rows are inserted at the same, then the inserted and deleted "tables" have multiple rows.
That part is simple. You will need to rewrite the trigger to take this into account.
On checking my web code the update button was performing additional updates, this caused the trigger to fire more than once thus causing duplicate rows.
create table dbo.lkp_asset_types_test
(
at_id int identity,
at_typedesc varchar(100)
)
go
create trigger trg_InsteadOfUpdate_test on [dbo].[lkp_asset_types_test]
instead of update
as
begin
select 'trigger fired!!!!'
if not exists(select * from inserted)
and not exists(select * from deleted)
begin
return;
end
--update (maybe only the diffs?)
update t
set at_typedesc = i.at_typedesc
from dbo.lkp_asset_types_test as t
join inserted as i on t.at_id = i.at_id;
--where t.at_typedesc <> i.at_typedesc & nulls??
--insert into sql_log(sql_log)
select
'SQL PRE Changed from : ' + isnull(d.at_typedesc, '*null*') + ' to: ' + isnull(i.at_typedesc, '*null*') + '. Action = U'
from inserted as i
join deleted as d on i.at_id = d.at_id
--where i.at_typedesc <> d.at_typedesc & nulls ??
end
go
insert into dbo.lkp_asset_types_test(at_typedesc) values ('A'), ('B'), ('C'), ('D'), (NULL);
go
update dbo.lkp_asset_types_test
set at_typedesc = case when at_id%2=0 then isnull(at_typedesc, 'X') else isnull(at_typedesc, '') + 'xyz' end
go
select *
from dbo.lkp_asset_types_test;
go
update dbo.lkp_asset_types_test
set at_typedesc = case when at_id%2=0 then at_typedesc else at_typedesc + 'xyz' end
where 1=2
go
--
drop table lkp_asset_types_test;

Alternative to Iteration for INSERT SELECT UPDATE in a sequence

I have a table with around 17k unique rows for which I need to run these set of statements in sequence
INSERT INTO TABLE1 using MASTERTABLE data (MASTERTABLE have 6 column)
SELECT value of column ID (Primary Key) of newly inserted row from TABLE1
Update that ID value in TABLE2 using a Stored Procedure
I have tried:
while loop: took around 3 hours to complete the execution
cursor: cancelled the query after executing it overnight
In my understanding I can not use JOIN as I need to execute the statements in a sequence
The questions is not detailed enough. The general idea I would like to use something like this
-- create a output table to hold new id, and key columns to join later
DECLARE #OutputTbl TABLE (ID INT, key_Columns in MASTERTABLE)
INSERT INTO TABLE1
OUTPUT INSERTED.ID, MASTERTABLE.key_columns INTO #OutputTbl
SELECT *
FROM MASTERTABLE
UPDATE T2
SET ID = o.ID
FROM TABLE2 t2
INNER JOIN OutputTbl o
ON t2.key_column = o.key_column
Maybe you can consider a TRIGGER on TABLE1 from which to call the stored procedure on TABLE2, and then you can call your INSERT as you wish/need.. one by one or in blocks..
DROP TRIGGER TR_UPD_TABLE2
GO
CREATE TRIGGER TR_UPD_TABLE2 ON TABLE1 AFTER INSERT
AS
BEGIN
SET NOCOUNT ON
DECLARE #columnID INT = NULL
IF (SELECT COUNT(*) FROM INSERTED)=1 BEGIN
-- SINGLE INSERT
SET #columnID = (SELECT columnID FROM INSERTED)
EXEC TableTwoUpdateProcedure #columnID
END ELSE BEGIN
-- MASSIVE INSERT (IF NEEDED)
SET #columnID = 0
WHILE #columnID IS NOT NULL BEGIN
SET #columnID = (SELECT MIN(columnID) FROM INSERTED WHERE columnID > #columnID)
IF #columnID IS NOT NULL BEGIN
EXEC TableTwoUpdateProcedure #columnID
END
END
END
END

SQL Server 2012 trigger: Execute dynamic sql for each row

I have a trigger in a table which keeps all the changes (Insert, Update, Delete). When I insert only one row per time it works fine. But when I am trying to insert multiple rows at once I receive this error :
Subquery returned more than 1 value. This is not permitted when the
subquery follows =, !=, <, <= , >, >= or when the subquery is used as
an expression.
Here is the code of the trigger ( I removed some parts that are not needed to shorten the code like variable declarations etc.)
UPDATE: The actual error is in these lines when #tempTrigT contains more than one rows:
Select * into #tempTrigT from (select * from deleted where #Action in ( 'U','D')) A UNION (select * from inserted where #Action ='I')
set #sql = 'set #audit_oldvalue=(select cast([' +#Item +'] as NVARCHAR(4000)) from #tempTrigT)';
EXEC SP_EXECUTESQL #sql,N'#audit_oldvalue sql_variant OUTPUT',#audit_oldvalue OUTPUT -- If inserted #audit_oldvalue gets the new value
set #sql = 'set #audit_value=(select cast(i.[' +#Item +'] as NVARCHAR(4000)) from dbo.TForms i inner join #tempTrigT d on i.id = d.id)';
EXEC SP_EXECUTESQL #sql,N'#audit_value sql_variant OUTPUT',#audit_value OUTPUT
How I can change it to work for multiple rows as well?
You are missing a row identifier so that you are only handling one row per loop. Something like:
DECLARE #ID int = (SELECT MIN(id) FROM #tempTrigT) to define a row at the start of your loop
WHERE id = #ID to filter to this row throughout the loop
DELETE FROM #tempTrigT WHERE id = #ID at the end of your loop, when that id is done processing
Then again, that may not even work if the id can repeat in #tempTrigT.
And with all that said...
I would definitely consider separating this into multiple triggers and save yourself the complexity you are facing by trying to loop through deleted or inserted records and handle them all accordingly. I would also consider simplifying your audit process. The end goal is to be able to look back at what records used to be, which you can achieve really simply:
INSERT INTO [dbo].[AuditTrailTForms] (TForms_Cols, ChangeDate, Change_User, Change_Type)
SELECT T.*, GETDATE(), COALESCE(ModifiedBy,suser_name()), 'Inserted'
FROM inserted i
JOIN TForms T on i.id = T.id
Then you can worry about making it easier to view which column values changed later on when you query these tables:
SELECT *
FROM (SELECT *, GETDATE(), 'Current', 'Current'
FROM TForms
WHERE ID = #AuditID
UNION ALL
SELECT *
FROM AuditTrailTForms
WHERE ID = #AuditID
--AND Change_Type =
--AND Change_User =
) T
ORDER BY ChangeDate DESC
Edit: Using an identity column:
You can use an identity column to define a row for each loop like so:
DECLARE #TotalRows int = (SELECT MAX(identityColumn) FROM #tempTrigT
DECLARE #RowID int = 1
WHILE #RowID <= #TotalRows
BEGIN
--Do stuff
--For Example
SET #sql = 'set #audit_oldvalue=(SELECT cast([' +#Item +'] as NVARCHAR(4000))
FROM #tempTrigT
WHERE T.IdentityColumn = #RowID)';
EXEC SP_EXECUTESQL #sql,N'#audit_oldvalue sql_variant OUTPUT',#audit_oldvalue OUTPUT
--then increment to the next row when you're done
SET #RowID = #RowID + 1
END

I need to optimize my first T-SQL update trigger

How do I rewrite this update trigger without using a lot of variables?
I wrote my first SQL Server trigger and it works fine, but I think, that there must be an easier solution.
If minimum one of 5 columns is changed I write two new rows in another table.
row 1 = old Fahrer (=Driver) and old dispodate and update-time
row 2 = new Fahrer and new dispodate and updatedatetime
My solution is just a copy of the foxpro-trigger, but there must be a easier solutions in T-SQL to check whether one colum is changed.
ALTER TRIGGER [dbo].[MyTrigger]
ON [dbo].[tbldisposaetze]
AFTER UPDATE
AS
SET NOCOUNT ON;
/*SET XACT_ABORT ON
SET ARITHABORT ON
*/
DECLARE #oldfahrer varchar(10)
DECLARE #oldbus varchar(10)
DECLARE #olddispodat date
DECLARE #oldvzeit decimal(4,0)
DECLARE #oldbzeit decimal(4,0)
DECLARE #oldbeschreibk varchar(255)
DECLARE #newfahrer varchar(10)
DECLARE #newbus varchar(10)
DECLARE #newdispodat date
DECLARE #newvzeit decimal(4,0)
DECLARE #newbzeit decimal(4,0)
DECLARE #newbeschreibk varchar(255)
SELECT #oldfahrer = fahrer,#oldbeschreibk=beschreibk,#oldbus=bus,#oldbzeit=bzeit,#olddispodat=dispodat,#oldvzeit=vzeit
FROM DELETED D
SELECT #newfahrer = fahrer,#newbeschreibk=beschreibk,#newbus=bus,#newbzeit=bzeit,#newdispodat=dispodat,#newvzeit=vzeit
FROM inserted I
if #oldbeschreibk <> #newbeschreibk or #oldbus <> #newbus or #oldbzeit <> #newbzeit or #oldfahrer <> #newfahrer or #oldvzeit <> #newvzeit
begin
IF (SELECT COUNT(*) FROM tbldispofahrer where fahrer=#oldfahrer and dispodat=#olddispodat) > 0
update tbldispofahrer set laenderung = GETDATE() where fahrer=#oldfahrer and dispodat=#olddispodat
else
INSERT into tbldispofahrer (fahrer,dispodat,laenderung) VALUES (#oldfahrer,#olddispodat,getdate())
IF (SELECT COUNT(*) FROM tbldispofahrer where fahrer=#newfahrer and dispodat=#newdispodat) > 0
update tbldispofahrer set laenderung = GETDATE() where fahrer=#newfahrer and dispodat=#newdispodat
else
INSERT into tbldispofahrer (fahrer,dispodat,laenderung) VALUES (#newfahrer,#newdispodat,getdate())
end
I'll assume you have SQL Server 2008 or greater. You can do this all in one statement without any variables.
Instead of doing all the work to first get the variables and see if they don't match, you can easily do that in as part of where clause. As folks have said in the comments, you can have multiple rows as part of inserted and deleted. In order to make sure you're working with the same updated row, you need to match by the primary key.
In order to insert or update the row, I'm using a MERGE statement. The source of the merge is a union with the where clause above, the top table in the union has the older fahrer, and the bottom has the new farher. Just like your inner IFs, existing rows are matched on farher and dispodat, and inserted or updated appropriately.
One thing I noticed, is that in your example newfahrer and oldfahrer could be exactly the same, so that only one insert or update should occur (i.e. if only bzeit was different). The union should prevent duplicate data from trying to get inserted. I do believe merge will error if there was.
MERGE tbldispofahrer AS tgt
USING (
SELECT d.farher, d.dispodat, GETDATE() [laenderung]
INNER JOIN inserted i ON i.PrimaryKey = d.PrimaryKey
AND (i.fahrer <> d.fahrer OR i.beschreibk <> d.beschreik ... )
UNION
SELECT i.farher, i.dispodat, GETDATE() [laenderung]
INNER JOIN inserted i ON i.PrimaryKey = d.PrimaryKey
AND (i.fahrer <> d.fahrer OR i.beschreibk <> d.beschreik ... )
) AS src (farher, dispodat, laenderung)
ON tgt.farher = src.farher AND tgt.dispodat = src.dispodat
WHEN MATCHED THEN UPDATE SET
laenderung = GETDATE()
WHEN NOT MATCHED THEN
INSERT (fahrer,dispodat,laenderung)
VALUES (src.fahrer, src.dispodat, src.laenderung)
There were a few little syntax errors in the answer from Daniel.
The following code is running fine:
MERGE tbldispofahrer AS tgt
USING (
SELECT d.fahrer, d.dispodat, GETDATE() [laenderung] from deleted d
INNER JOIN inserted i ON i.satznr = d.satznr
AND (i.fahrer <> d.fahrer OR i.beschreibk <> d.beschreibk or i.bus <> d.bus or i.bzeit <> d.bzeit or i.vzeit <> d.vzeit)
UNION
SELECT i.fahrer, i.dispodat, GETDATE() [laenderung] from inserted i
INNER JOIN deleted d ON i.satznr = d.satznr
AND (i.fahrer <> d.fahrer OR i.beschreibk <> d.beschreibk or i.bus <> d.bus or i.bzeit <> d.bzeit or i.vzeit <> d.vzeit)
) AS src (fahrer, dispodat, laenderung)
ON tgt.fahrer = src.fahrer AND tgt.dispodat = src.dispodat
WHEN MATCHED THEN UPDATE SET
laenderung = GETDATE()
WHEN NOT MATCHED THEN
INSERT (fahrer,dispodat,laenderung)
VALUES (src.fahrer, src.dispodat, src.laenderung);

Update multiple rows in table from table variable

I'm writing a stored procedure to update multiple records based on a table variable parameter.
The existing table is: Tb_Project_Image with relevant columns:
id PK (identity 1,1)
cat_ord decimal(4,2)
The procedure will receive a temporary table variable (shown in the code below) containing the id as PI_ID, and the new value for cat_ord as newCatOrd. idx is a simple identity for each row containing 1...n where n is the rowcount of #tempTable.
For each row in #tempTable, I want to update Tb_Project_Image where id = PI_ID to the corresponding value.
DECLARE #tempTable table (
idx smallint Primary Key IDENTITY(1,1),
PI_ID bigint,
newCatOrd decimal(4, 2) not null )
INSERT INTO #tempTable values (3, 7.01)
INSERT INTO #tempTable values (4, 7.02)
INSERT INTO #tempTable values (5, 7.03)
--etc...
DECLARE #error int
DECLARE #update int
DECLARE #iter int
SET #iter = 1
BEGIN TRAN
WHILE #iter <= (select COUNT(*) from #tempTable)
BEGIN
UPDATE Tb_Project_Image
SET cat_ord = (SELECT newCatOrd FROM #tempTable
WHERE idx = #iter)
WHERE id = (SELECT PI_ID FROM #tempTable
WHERE idx = #iter)
--error checking
set #error = ##ERROR
set #update = ##ROWCOUNT
IF ((#error = 0) AND (#update = 1))
BEGIN
SET #iter = #iter + 1
CONTINUE
END
ELSE
BREAK
END
IF ((#error = 0) AND (#update = 1))
COMMIT TRAN
ELSE
ROLLBACK TRAN
GO
Now, the error checking is because, to ensure integrity, EACH row in the temporary table MUST make 1 update. (explanation omitted to save space) If a single iteration of the while loop threw an error, or didn't effect exactly 1 row, I want to break the loop and rollback the transaction
THE PROBLEM I'm having is that this error checking is not working. I'm currently running it with 14 rows in #tempTable and the 11th uses a PI_ID not found in the Project_Image table. Therefore, #update = 0... but it continues the loop and commits the data.
I'd be doubly glad if someone had a method of doing this that only used a single update statement.
You cannot do it this way, because even SET resets the state of ##ERROR and ##ROWNUMBER variables. In this case ##ROWCOUNT is set to 1 after set #error = ##ERROR. If you do not assign the values to local variables, your code will work:
IF ((##error = 0) AND (##rowcount = 1))
But you might rather try try...catch error handling and test ##rowcount separately after update.
UPDATE: doing it in single update:
UPDATE t
SET cat_ord = tt.newCatOrd
FROM Tb_Project_Image t
INNER JOIN #tempTable tt
ON t.id = tt.PI_ID
-- If there was PI_ID not found in Tb_Project_Image
-- But I think that this should have been dealt with
-- During the initial loading of temporary table
IF ##ROWCOUNT <> (select count (*) from #tempTable)
BEGIN
-- Error reporting here
ROLLBACK TRANSACTION
END
Instead of updating and then rolling back, you could also use a CTE to determine if any records should be updated prior to performing the update. Something like this should work:
WITH NON_SINGLETON AS (
-- Find any records in #tempTable that don't match
-- exactly one record in Tb_Project_Image
SELECT t.PI_ID, COUNT(pi.id) C
FROM #tempTable t
LEFT JOIN Tb_Project_Image pi ON t.PI_ID = pi.id
GROUP BY t.PI_ID
HAVING COUNT(pi.id) != 1
)
UPDATE Tb_Project_Image
SET cat_ord = t.newCatOrd
FROM Tb_Project_Image pi
JOIN #tempTable t ON pi.id = t.PI_ID
-- If any invalid records were found in the CTE,
-- then this condition will fail for all rows
-- and nothing will be updated
WHERE NOT EXISTS(SELECT 1 FROM NON_SINGLETON)
If it's possible for #tempTable to have duplicate entries for the same PI_ID, then this will handle those scenarios as well. And since it's a single statement, you don't have to explicitly managing the transaction in the proc (if it's the only thing that needs to be included in the transaction).