Maybe my original post was little messy, so I didn't get much help. I updated my trigger with the AuditTest table.... Please see if you can help.
I am very new to triggers and trying to put together an audit table where it tracks the value changes on certain columns.
I have a lot of columns like: Qty, UnitSell, Discount, ProductName ...etc. This code below seems to work, which detects the Qty or UnitSell value changes, then do the INSERT INTO the audit table (now I just named it as TEST1)
If I would keep repeating the IF UPDATE(FieldName) statement for other columns, I think it will work, but it is too cumbersome in which keep repeating the same codes.
Is that way to optimize this, so I don't have to repeating the same IF UPDATE (Fieldname) statement?
alter TRIGGER trigger_Test_AfterUpdate
ON [dbo].ERP_QuoteDetail
FOR UPDATE
AS
declare #QuoteDetailID int;
select #QuoteDetailID = i.QuoteDetailID from inserted i;
--- Updating QTY if old/new value change
DECLARE #iQty int; SELECT #iQty = i.Qty from inserted i;
DECLARE #dQty int; SELECT #dQty = d.Qty from deleted d;
if update(QTY) and exists (select * from deleted d WHERE Qty <> #iQty)
BEGIN
-- Insert into the audit table
insert into AuditTest (
[Type]
,[TableName]
,[PKCol]
,[PK]
,[FieldName]
,[OldValue]
,[NewValue]
,[UpdateDate]
,[DBUsername]
,[UserID]
)
values('u'
, 'Table_QuoteDetail'
, 'QuoteDetail'
, #QuoteDetailID
, 'QTY'
, #dQty
, #iQty
, GETDATE()
, '123'
, '456'
);
PRINT 'AFTER UPDATE Trigger fired.'
END
--- Updating QTY if old/new value change
DECLARE #iUnitSell int; SELECT #iUnitSell = i.UnitSell from inserted i;
DECLARE #dUnitSell int; SELECT #dUnitSell = d.Qty from deleted d;
if update(UnitSell) and exists (select * from deleted d
WHERE UnitSell <> #iUnitSell )
BEGIN
-- Insert into the audit table
insert into AuditTest (
[Type]
,[TableName]
,[PKCol]
,[PK]
,[FieldName]
,[OldValue]
,[NewValue]
,[UpdateDate]
,[DBUsername]
,[UserID]
)
values('u'
, 'Table_QuoteDetail'
, 'QuoteDetail'
, #QuoteDetailID
, 'UnitSell'
, #dUnitSell
, #iUnitSell
, GETDATE()
, '123'
, '456'
);
PRINT 'AFTER UPDATE Trigger fired.'
END
GO
You can consider using the Change Data Capture feature of SQL Server for auditing. CDC stores a record of all inserted, updated and deleted rows for whichever tables have CDC enabled. This will effectively replace the need for triggers in your case.
Details on how to enable and configure CDC can be found here.
Your fundamental flaw is that you seem to expect the trigger to be fired once per row - this is NOT the case in SQL Server. Instead, the trigger fires once per statement, and the pseudo table Deleted might contain multiple rows.
Given that that table might contain multiple rows - which one do you expect will be selected here??
select #QuoteDetailID = i.QuoteDetailID from inserted i;
It's undefined - you might get the values from arbitrary rows in Inserted.
You need to rewrite your entire trigger with the knowledge that the Inserted and Deleted pseudo tables WILL contain multiple rows. You need to work with set-based operations - don't expect just a single row in Deleted or Inserted !
Related
I have a trigger on my tbl_permissions table.
Trigger is;
ALTER TRIGGER [dbo].[trig_permissions] ON [dbo].[tbl_permissions]
FOR UPDATE
AS
IF(UPDATE(email) OR UPDATE(gsm))
BEGIN
INSERT INTO dbo.tbl_permissions_log
(customer_id,type,email_new_value,email_old_value,gsm_new_value,gsm_oldu_value,modify_user_id,modify_date)
SELECT
i.customer_id,
d.type,
i.email,
d.email,
i.gsm,
d.gsm,
i.modify_user_id,
GETDATE()
FROM inserted i, deleted d ,dbo.tbl_permissions c
WHERE c.pk_id = i.pk_id AND c.pk_id = d.pk_id AND
(RTRIM(d.email) <> RTRIM(i.email)
OR (RTRIM(d.gsm) <> RTRIM(i.gsm)))
The trigger is triggering in 2 store procedure.
First One;
ALTER PROC [dbo].[sp_activation_email_update]
(
#sEmail NVARCHAR(100),
#lModifyUserId INT,
#bEML BIT
)
AS
BEGIN
UPDATE dbo.tbl_permissions
SET email=#bEML,
modify_user_id=#lModifyUserId,
modify_date=GETDATE()
WHERE customer_id
IN(SELECT customer_id FROM dbo.tbl_contact_info WHERE email=#sEmail)
END
Second;
ALTER PROC [dbo].[sp_activation_sms_update]
(
#sGsmNo NVARCHAR(15),
#lModifyUserId INT,
#bGsm BIT
)
AS
BEGIN
UPDATE dbo.tbl_permissions
SET gsm=#bGsm,
modify_user_id=#lModifyUserId,
modify_date=GETDATE()
WHERE customer_id
IN(SELECT customer_id FROM dbo.tbl_contact_info WHERE gsm_no=RIGHT(#sGsmNo, 10))
END
I want to remove trigger because of performance problem. So How can I perform the work of the trigger in the store procedure?
I tried the call another store procedure inside update store procedures and perform operations in the this new store procedure but I cant.
In your stored procedures, you can use the OUTPUT clause to capture data from the UPDATE statement, and then use that captured data to insert rows into the log table.
Something like the following:
DECLARE #Updated TABLE (pk_id int, newEmail, oldEmail, modify_user_id int)
UPDATE dbo.tbl_permissions
SET email=#bEML,
modify_user_id=#lModifyUserId,
modify_date=GETDATE()
OUTPUT inserted.pk_id, inserted.email, deleted.email, u.modify_user_id
INTO #Updated
WHERE customer_id
IN(SELECT customer_id FROM dbo.tbl_contact_info WHERE email=#sEmail)
INSERT INTO dbo.tbl_permissions_log
(customer_id,type,email_new_value,email_old_value,gsm_new_value,gsm_oldu_value,modify_user_id,modify_date)
SELECT
p.customer_id,
p.type,
u.newEmail,
u.oldEmail,
p.gsm,
p.gsm,
u.modify_user_id,
GETDATE()
FROM #Updated u
JOIN dbo.tbl_permissions p ON p.pk_id = u.pk_id
WHERE u.newEmail <> u.oldEmail
You can also direct output directly into the log table, but the statement is getting pretty cluttered at that point.
UPDATE dbo.tbl_permissions
SET email=#bEML,
modify_user_id=#lModifyUserId,
modify_date=GETDATE()
OUTPUT
inserted.customer_id,
inserted.type,
inserted.email,
deleted.email,
inserted.gsm,
deleted.gsm,
inserted.modify_user_id,
GETDATE()
INTO dbo.tbl_permissions_log
(customer_id,type,email_new_value,email_old_value,gsm_new_value,gsm_oldu_value,modify_user_id,modify_date)
WHERE customer_id
IN(SELECT customer_id FROM dbo.tbl_contact_info WHERE email=#sEmail)
AND email <> #bEML
In both cases we limit the log insert to cases where the updated value actually changed. In the latter case, this necessitates applying that condition to the actual update statement. This would modify the original behavior by also inhibiting the update to modify_user_id and modify_date when email is unchanged. (This might be a positive change.)
I can detect duplicate records, but when I'm inserting new data it will detect it as a duplicate record even if doesn't already exist.
Here is my code:
ALTER TRIGGER [dbo].[SDPRawInventory_Dup_Trigger]
ON [dbo].[SDPRawInventory]
AFTER INSERT
AS
DECLARE #Year float,
#Month float,
#SDPGroup nvarchar(255);
SELECT
#Year = i.InvYear, #Month = i.InvMonth, #SDPGroup = i.SDPGroup
FROM inserted i;
IF (SELECT COUNT(*) FROM SDPRawInventory A
WHERE A.InvYear = #Year
AND A.InvMonth = #Month
AND A.SDPGroup = #SDPGroup) >= 1
BEGIN
RAISERROR ('Duplicate data', 16, 1)
ROLLBACK;
END
ELSE
BEGIN
INSERT INTO SDPRawInventory
SELECT * FROM inserted;
END
This is the table
And to clarify there is no primary key nor unique identifier.
If you are unable to put a constraint in place, then you need to handle the fact that Inserted may have multiple records. And because its an after insert trigger, you don't need to do anything if no duplicates are found because the records are already inserted.
ALTER TRIGGER [dbo].[SDPRawInventory_Dup_Trigger]
ON [dbo].[SDPRawInventory]
AFTER INSERT
AS
BEGIN
SET NOCOUNT ON;
IF EXISTS (
SELECT 1
FROM dbo.SDPRawInventory S
INNER JOIN Inserted I ON
-- Test for a duplicate
S.InvYear = I.InvYear
AND S.InvMonth = I.InvMonth
AND S.SDPGroup = I.SDPGroup
-- But ensure the duplicate is a *different* record - assumes a unique ID
AND S.ID <> I.ID
)
BEGIN
THROW 51000, 'Duplicate data.', 1;
END;
END;
Note the simplified and modern error handling.
EDIT: And if you have no unique key, and no permission to add one, then you need an instead of trigger to only insert non-duplicates e.g.
ALTER TRIGGER [dbo].[SDPRawInventory_Dup_Trigger]
ON [dbo].[SDPRawInventory]
INSTEAD OF INSERT
AS
BEGIN
SET NOCOUNT ON;
-- Reject the entire insert if a single duplicate exists
-- Note if multiple records are inserted, some of which are duplicates and some of which aren't, they all get rejected
IF EXISTS (
SELECT 1
FROM dbo.SDPRawInventory S
INNER JOIN Inserted I ON
-- Test for a duplicate
A.InvYear = I.InvYear
AND A.InvMonth = I.InvMonth
AND A.SDPGroup = I.#SDPGroup
)
-- Test that Inserted itself doesn't contain duplicates
OR EXISTS (SELECT 1 FROM Inserted GROUP BY InvYear, InvMonth, SDPGroup HAVING COUNT(*) > 1)
BEGIN
THROW 51000, 'Duplicate data.', 1;
END;
INSERT INTO dbo.SDPRawInventory (SDP_SKU_DESC, WholeQty, InvYear, InvMonth, SDPGroup, invUOM, LooseQty)
SELECT SDP_SKU_DESC, WholeQty, InvYear, InvMonth, SDPGroup, invUOM, LooseQty
FROM Inserted I
WHERE NOT EXISTS (
SELECT 1
FROM dbo.SDPRawInventory S
-- Test for a duplicate
WHERE S.InvYear = I.InvYear
AND S.InvMonth = I.InvMonth
AND S.SDPGroup = I.SDPGroup
);
END;
Note: This doesn't do anything to handle existing duplicates.
This trigger is executed after the new records were inserted, so it will at least find the original records in the SELECT COUNT statement. Changing >= 1 into >= 2 can only partially fix this when inserting is guaranteed to occur one record as a time. Moreover, it will still fail when there were already multiple duplicated of the newly inserted record in the database before the insert.
You need to exclude the latest inserted records from the COUNT. But a better idea would probably be to add a UNIQUE constraint for preventing duplicates, so no trigger would be necessary.
If adding a constraint is not possible yet, you should initiate a clean-up process to eliminate the existing duplicates beforehand. Everything else is looks pretty flawed to me, since it is unlikely the current approach will ever bring the table into a state with no duplicates.
You are creating the infinite loop. You just have to remove the insertion part from your trigger.
ELSE
BEGIN
INSERT INTO SDPRawInventory
SELECT * FROM inserted;
END
This should not be in the trigger as trigger is called as part of insertion. you should not write actual insertion in to table in trigger.
I recently ran an update query on all the rows (Approx. 25k)...
My update query was a simple update as shown here:
Update om_Challans set LockType = 'Locked', LockActionDate = getdate(), LockActionBy = 'Query'
I have not updated any other column at all.
I also have a trigger that keeps the history in 'om_challans_history' table, so any row that changes is moved to the history table.
I recently noticed that data in about 26 rows have been changed to ZERO automatically by the query. Here is the sample of what i mean:
Any idea of how is that possible will be greatly appreciated
Update
Here is the trigger on om_Challans Table
ALTER TRIGGER [dbo].[T_CreateUpdateHistory1]
on [dbo].[OM_Challans]
after update
as
set nocount on
if ((select [Status] from deleted) LIKE '%FieldData%')
BEGIN
return
END
insert into om_challans_history (
[Rec_Ltr1] ,
[challan_id] ,
[LockType],
[LockActionDate],
[LockActionBy],
[SamplesSent]
) /* Columns in OM_Challans Table */
select
i.Rec_Ltr1 ,
i.challan_id ,
i.LockType,
i.LockActionDate,
i.LockActionBy,
i.SamplesSent /* Old Values of this table */
from
OM_Challans a
inner join
deleted i
on
a.sno = i.sno /* Primary Key columns from table A */
Im trying to make this trigger work when trying to delete a record. The way it is suposed to work is, when someone tries to delete a record it rollbacks and inserts an audit record to TbAudit table which by the way, all columns have a NOT NULL constraint. However, turns out it wont do it, because for some reason I dont understand when I try to delete a record it will display the message and rollback BUT all my variables within the select statement are getting NULL values even though Im pulling them directly from the "deleted" table. Please help.
USE BdPlan
GO
CREATE TRIGGER TrAudit
ON Plan.TPlan
AFTER DELETE
AS
BEGIN
DECLARE #IdPlan = int,
#IdEmployee int,
#Month int,
#Year int
ROLLBACK
PRINT 'CANT DELETE RECORDS'
-- All variables are getting NULL
SELECT #IdPlan = D.IdPlan,
#IdEmployee = D.IdEmployee ,
#Month = D.Month,
#Year = D.Year
FROM deleted AS D
INSERT INTO BdAudit.dbo.TbAudit
VALUES
(
#IdPlan,
#IdEmployee,
#Month,
#Year,
SUSER_NAME(),
GETDATE()
)
END
I believe there may be problems with this approach:
you are trying to access the DELETED pseudotable after the transaction has been rolled back - it will have zero rows after the rollback (see below)
your trigger only attempts to deal with a single row deletion - it should be able to handle multi row deletes
It is also noted that inserting directly into the Audit table from the Deleted pseudotable before ROLLBACK will of course roll the audit data back as well.
From here it is apparent you can cache the data to be audited in a #Temporary table variable, then do the ROLLBACK (which doesn't undo the #Temp table), and then do the Audit insertion:
ALTER trigger d_foo ON FOO AFTER DELETE
AS BEGIN
DECLARE #Temp AS TABLE
(
ID INT,
-- Obviously add all your fields go here
);
INSERT INTO #Temp(ID)
SELECT ID FROM DELETED;
ROLLBACK TRAN;
insert into fooaudit(id)
select id from #Temp;
END;
Simplified SqlFiddle here with multiple row deletion.
To confirm, the DELETED pseudotable contains zero rows after a ROLLBACK in a trigger, as this modified Fiddle demonstrates.
I would like to know which row has fired a trigger on a table.
The reason therefore is that I would like to backup only the changed row to a backup table.
For example I have a table with the fields ID, NAME, ADDRESS, CITY and when one of those fields has updated, deleted or inserted I would make a copy of that row to the backup table, but only that changed row, not the whole table.
Creating a trigger that makes a backup of the complete table is easy but I can't find a solution to backup only the changed row.
Look at the inserted table within the trigger - it shows inserts and updates - see http://msdn.microsoft.com/en-us/library/ms191300.aspx for an example
According to the CREATE TRIGGER documentation:
DML triggers use the deleted and inserted logical (conceptual)
tables. They are structurally similar to the table on which the
trigger is defined, that is, the table on which the user action is
tried. The deleted and inserted tables hold the old values or new
values of the rows that may be changed by the user action.
SQL Server exposes within a trigger the changes made to a table through two "virtual" tables named deleted and inserted. In case of an insert operation inserted contains the newly inserted data and deleted is empty, in case of a delete operation inserted is empty and the deleted table contains the deleted rows. In case of an update operation the inserted table contains the changed rows after the update and deleted the rows before the update.
Both tables are structurally identical to the base table. You can use the function update()
in case of an update event, to check if some specific column was updated.
I came to the same solution, hereby the code I used to accomplish the task:
USE [MyTable]
GO
/**** Object: Trigger [dbo].[trg_MappingHistory] Script Date: 03/20/2012 09:08:34 ****/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TRIGGER [dbo].[trg_MappingHistory] ON [dbo].[Mappings]
AFTER INSERT, DELETE, UPDATE
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON ;
DECLARE #HistoriekNrNew INT ;
SELECT #HistoriekNrNew = MAX([dbo].[MappingsHistoriek].[HistoriekNr]) + 1
FROM [dbo].[MappingsHistoriek]
IF #HistoriekNrNew IS NULL
BEGIN
SET #HistoriekNrNew = 1
END
-- Insert statements for trigger here
INSERT INTO MappingsHistoriek
( [dbo].[MappingsHistoriek].[EntiteitNaam] ,
[dbo].[MappingsHistoriek].[AppID] ,
[dbo].[MappingsHistoriek].[LijstID] ,
[dbo].[MappingsHistoriek].[Versie] ,
[dbo].[MappingsHistoriek].[LijstNaam] ,
[dbo].[MappingsHistoriek].[Waarde] ,
[dbo].[MappingsHistoriek].[Type] ,
[dbo].[MappingsHistoriek].[Datum] ,
[dbo].[MappingsHistoriek].[Gebruiker] ,
[dbo].[MappingsHistoriek].[HistoriekNr],
[dbo].[MappingsHistoriek].[Actie]
)
SELECT [dbo].[Entiteit].[Naam] ,
deleted.[AppID] ,
deleted.[LijstID] ,
deleted.[LijstNaam] ,
deleted.[Versie] ,
deleted.[Waarde] ,
deleted.[Type] ,
GETDATE() ,
SYSTEM_USER ,
#HistoriekNrNew ,
'DELETED'
FROM deleted
INNER JOIN [dbo].[Entiteit] ON Entiteit.ID = deleted.AppID
INSERT INTO MappingsHistoriek
( [dbo].[MappingsHistoriek].[EntiteitNaam] ,
[dbo].[MappingsHistoriek].[AppID] ,
[dbo].[MappingsHistoriek].[LijstID] ,
[dbo].[MappingsHistoriek].[Versie] ,
[dbo].[MappingsHistoriek].[LijstNaam] ,
[dbo].[MappingsHistoriek].[Waarde] ,
[dbo].[MappingsHistoriek].[Type] ,
[dbo].[MappingsHistoriek].[Datum] ,
[dbo].[MappingsHistoriek].[Gebruiker] ,
[dbo].[MappingsHistoriek].[HistoriekNr],
[dbo].[MappingsHistoriek].[Actie]
)
SELECT [dbo].[Entiteit].[Naam] ,
inserted.[AppID] ,
inserted.[LijstID] ,
inserted.[LijstNaam] ,
inserted.[Versie] ,
inserted.[Waarde] ,
inserted.[Type] ,
GETDATE() ,
SYSTEM_USER ,
#HistoriekNrNew ,
'INSERTED'
FROM inserted
INNER JOIN [dbo].[Entiteit] ON Entiteit.ID = inserted.AppID
END
GO