Audit trigger inserting multiple rows into audit table - sql

I have written a trigger and when I update a value in the base table, I get more than one record in my audit table. I'm expecting just one. Here's my trigger:
CREATE TRIGGER dbo.SOP10100_TRDISAMT
ON dbo.SOP10100
AFTER INSERT, UPDATE
AS
BEGIN
SET NOCOUNT ON;
IF (select TRDISAMT from inserted) = 0
BEGIN
RETURN
END
ELSE
BEGIN
DECLARE #SOPTYPE smallint = (select soptype from inserted)
DECLARE #SOPNUMBE char(21) = (select sopnumbe from inserted)
DECLARE #MSTRNUMB int = (select mstrnumb from inserted)
DECLARE #DOCID char(15) = (select docid from inserted)
DECLARE #SUBTOTAL numeric(19,5) = (select subtotal from inserted)
DECLARE #MISCAMT numeric(19,5) = (select MISCAMNT from inserted)
DECLARE #TRDISAMT numeric(19,5) = (select TRDISAMT from inserted)
DECLARE #TRDISAMT_B4 numeric(19,5) = (select TRDISAMT from deleted)
DECLARE #FRTAMNT numeric(19,5) = (select FRTAMNT from inserted)
DECLARE #TAXAMNT numeric(19,5) = (select TAXAMNT from inserted)
DECLARE #DOCAMNT numeric(19,5) = (select DOCAMNT from inserted)
DECLARE #USERNAME nvarchar(128) = (select SUSER_SNAME())
DECLARE #TheTime datetime = (select GETDATE())
INSERT INTO SOP10100_TRDISAMT_AUDIT (soptype,sopnumbe,MSTRNUMB,DOCID,SUBTOTAL,MISCAMT,TRDISAMT,TRDISAMT_B4,FRTAMNT,TAXAMNT,DOCAMNT,USERNAME,TheTime)
VALUES (#SOPTYPE,#SOPNUMBE,#MSTRNUMB,#DOCID,#SUBTOTAL,#MISCAMT,#TRDISAMT,#TRDISAMT_B4,#FRTAMNT,#TAXAMNT,#DOCAMNT,#USERNAME,#TheTime)
END
END
and here is what I run to update the value:
update SOP10100 set TRDISAMT = 35 where SOPTYPE = 1 and SOPNUMBE = '126535'
This results in two records being inserted into my audit table. Thoughts?

Your query is fatally flawed, because it does not take into account multiple (or zero) rows being inserted or updated.
Your trigger should probably look something like this:
CREATE TRIGGER dbo.SOP10100_TRDISAMT
ON dbo.SOP10100
AFTER INSERT, UPDATE
AS
INSERT INTO SOP10100_TRDISAMT_AUDIT
(soptype, sopnumbe, MSTRNUMB, DOCID, SUBTOTAL, MISCAMT, TRDISAMT,
TRDISAMT_B4, FRTAMNT, TAXAMNT, DOCAMNT, USERNAME, TheTime)
SELECT i.soptype, i.sopnumbe, i.mstrnumb, i.docid, i.subtotal, i.MISCAMNT, i.TRDISAMT,
i.TRDISAMT, i.FRTAMNT, i.TAXAMNT, i.DOCAMNT, SUSER_SNAME(), GETDATE()
FROM inserted i
WHERE i.TRDISAMT <> 0;
GO
If you want to exclude rows where the values have not been changed at all by the UPDATE statement, you need to add
EXCEPT
SELECT d.soptype, d.sopnumbe.....
FROM deleted d
I note that char is an unusual data type, and should only be used where the value is fixed at that length, otherwise you will get trailing spaces.

Related

SQL Server 2012: correct trigger does not fire after insert

I have 2 triggers for 2 tables. Both create numbers, one creates an articlenumber, the other an ordernumber.
Articlenumber:
ALTER TRIGGER [dbo].[trg_CreateArtikelnummer]
ON [dbo].[tblArtikel]
AFTER INSERT
AS
IF ((SELECT TRIGGER_NESTLEVEL()) < 2)
BEGIN
DECLARE #ArtikelID INT;
DECLARE #LfID INT; --supplierID
DECLARE #ProdID INT
-- ---Wein, Traubensaft, Sekt
SET #ArtikelID = (SELECT
CASE WHEN Artikelart = 'Wein' THEN 1
WHEN Artikelart = 'Traubensaft' THEN 2
WHEN Artikelart = 'Sekt' THEN 3
END AS ArtikelID
FROM inserted)
SET #LfID = (SELECT Lfid FROM inserted)
SET #ProdID = (SELECT ProdID FROM inserted)
SET #Prodid = (SELECT COUNT(ProdID)
FROM [dbo].[tblArtikel]
WHERE Lfid = #LfID)
UPDATE dbo.tblArtikel
SET Artikelnummer = CONVERT(INT, CONVERT(NCHAR(1), #ArtikelID) +
CONVERT(NCHAR(3), FORMAT(#LfID, '000')) + CONVERT(NCHAR(4), FORMAT(#ProdID,'0000')))
FROM inserted
INNER JOIN dbo.tblArtikel ON inserted.prodid = dbo.tblArtikel.prodid
This one fires IMMEDIATELY when inserting date in the table by hand (for test purpose) and displays the generated articlenumber instantely.
Ordernumber: should be build from the running number per year for each supplier, so 1703001 stands for year 2017, supplierID 03, ordernumber 001
ALTER TRIGGER [dbo].[trg_CreateBestellnummer]
ON [dbo].[tblOrders]
AFTER INSERT
AS
IF ((SELECT TRIGGER_NESTLEVEL()) < 2)
BEGIN
--set nocount on;
declare #Jahr int
declare #LfID int --supplierID
declare #OrderID int
declare #Order int
--declare #Bestelldatum date
set #Jahr=(select right(year(getdate()),2))
set #LfID=(SELECT Lfid from inserted)
--SELECT #OrderID=INSERTED.OrderID from inserted
set #Order=(SELECT count(OrderID) from [dbo].[tblOrders] where Lfid=#LfID and year(Bestelldatum)=Year(Getdate()))
Update dbo.tblOrders set Bestellnummer=convert(int,convert(nchar(2),#Jahr)+convert(nchar(2),FORMAT(#LfID, '00'))+convert(nchar(3),format(#Order,'000')))
from INSERTED inner join dbo.tblOrders on INSERTED.OrderID= dbo.tblOrders.OrderID
This one does NOT fire at once, when finishing inserting date into the table and does NOT display the generated ordernumber.
I get a red exclamation mark on the left side of the records saying something like "data are write protected. Execute query again".
When I click the execute button on the menue the the trigger fires and updates the correct ordernumber.
What can be the reason?
Thanks
Michael

SQL insert trigger condition statement and multiple rows

Could you please help me to finish my trigger. What i got so far:
CREATE TRIGGER [dbo].[atbl_Sales_OrdersLines_ITrigGG]
ON [dbo].[atbl_Sales_OrdersLines]
FOR INSERT
AS
BEGIN
DECLARE #ID INT = (SELECT ProductID
FROM INSERTED)
DECLARE #OrderedQ INT = (SELECT SUM(Amount)
FROM atbl_Sales_OrdersLines
WHERE ProductID = #ID)
DECLARE #CurrentQ INT = (SELECT Quantity
FROM atbl_Sales_Products
WHERE ProductID = #ID)
DECLARE #PossibleQ INT = (SELECT Amount
FROM INSERTED
WHERE ProductID = #ID)
IF (#CurrentQ - #OrderedQ >= #PossibleQ)
ELSE
END
I need to complete the code. Can not figure out how to do it. I need that if condition is met - trigger would allow the insert. If else, trigger would stop the insert/or rollback and prompt a message that quantity is not sufficient.
Also, will this code work if insert is multiple lines with different product ids?
Thanks.
Something like this might work. This trigger checks the products that are in the insert, summing the total that have been ordered (now and in the past), and if any of them exceed the available quantity, the whole transaction is rolled back. Whenever writing triggers, you want to avoid any assumptions that there is a single row being inserted/updated/deleted, and avoid cursors. You want to just use basic set based operations.
CREATE TRIGGER [dbo].[atbl_Sales_OrdersLines_ITrigGG]
ON [dbo].[atbl_Sales_OrdersLines]
FOR INSERT
AS
BEGIN
IF (exists (select 1 from (
select x.ProductId, totalOrdersQty, ISNULL(asp.Quantity, 0) PossibleQty from (
select i.ProductId, sum(aso.Amount) totalOrdersQty
from (select distinct ProductId from inserted) i
join atbl_Sales_OrdersLines aso on aso.ProductId = i.ProductId
group by productId) x
left join atbl_Sales_Product asp on asp.ProductId = x.ProductId
) x
where PossibleQty < totalOrdersQty))
BEGIN
RAISERROR ('Quantity is not sufficient' ,10,1)
ROLLBACK TRANSACTION
END
END
I still think this is a horrible idea.
Try this,
CREATE TRIGGER [dbo].[atbl_Sales_OrdersLines_ITrigGG]
ON [dbo].[atbl_Sales_OrdersLines]
INSTEAD OF INSERT --FOR INSERT
AS
BEGIN
DECLARE #ID INT = (SELECT ProductID
FROM INSERTED)
DECLARE #OrderedQ INT = (SELECT SUM(Amount)
FROM atbl_Sales_OrdersLines
WHERE ProductID = #ID)
DECLARE #CurrentQ INT = (SELECT Quantity
FROM atbl_Sales_Products
WHERE ProductID = #ID)
DECLARE #PossibleQ INT = (SELECT Amount
FROM INSERTED
WHERE ProductID = #ID)
IF (#CurrentQ - #OrderedQ >= #PossibleQ)
BEGIN
INSERT INTO YOURTABLE (COLUMN1, COLUMN2, COLUMN3, ..)
SELECT COLUMN1, COLUMN2, COLUMN3, ..
FROM inserted
END
ELSE
BEGIN
RAISERROR ('Quantity is not sufficient' ,10,1)
ROLLBACK TRANSACTION
END

SQL: How do I get the last updated row to use in a trigger

So I got this trigger that looks like this
CREATE TRIGGER trLoadingOvertime
ON trailerScheme
FOR UPDATE AS
IF (SELECT COUNT(*) FROM trailerScheme
WHERE DATEDIFF(mi, trailerScheme.expectedFinishTime, trailerScheme.finishTime) > 15) > 0
BEGIN
INSERT INTO errorTable2
SELECT trailerSchemeID FROM inserted
END
and between BEGIN and END I need to insert values into errorTable2 from the trailerScheme that was just updated.
I hope someone can help me
Update:
When I use this code it just give me NULL
DECLARE #id INT
SELECT
#id = deleted.trailerSchemeID
FROM
inserted
INNER JOIN
deleted
ON inserted.trailerSchemeID = deleted.trailerSchemeID
INSERT INTO errorTable2 VALUES(#id)
Are you looking for this?
CREATE TRIGGER trLoadingOvertime
ON trailerScheme
FOR UPDATE AS
INSERT INTO errorTable2
SELECT trailerSchemeID
FROM inserted
WHERE DATEDIFF(mi, expectedFinishTime, finishTime) > 15
Here is SQLFiddle demo.
Figured it out my self. Here is the correct code:
DROP TRIGGER trLoadingOvertime
GO
CREATE TRIGGER trLoadingOvertime
ON trailerScheme
FOR INSERT AS
IF EXISTS (SELECT expectedFinishTime, finishTime
FROM inserted
WHERE DATEDIFF(mi, expectedFinishTime, finishTime) > 15)
BEGIN
DECLARE #id INT
DECLARE #exFTime DATETIME
DECLARE #fTime DATETIME
DECLARE #delay INT
SET #id = (SELECT trailerSchemeID FROM inserted)
SET #exFTime = (SELECT expectedFinishTime FROM inserted)
SET #fTime = (SELECT finishTime FROM inserted)
SET #delay = (SELECT DATEDIFF(mi, #exFTime, #fTime))
INSERT INTO errorTable VALUES(#id, #delay)
END
I was trying to get inserted, but the problem was that this trigger was called by an update and therefore the inserted was empty.

Modify SQL trigger to work when inserted table contains more than one row

I've got a SQL trigger written for a table in SQL Server 2008. It works well when there is only one row in the 'inserted' table. How can I modify this trigger to work correctly when there are multiple rows? Performance is key here, so I'd like to stay away from cursors, temp tables, etc. (if possible).
Essentially the trigger checks to see if either the 'ClientID' or 'TemplateID' fields were changed. If they were, and the OriginalClientID or OriginalTemplateID fields are null, it populates them (thus setting the OriginalXXX fields once and only once so I can always see what the first values were).
CREATE TRIGGER [dbo].[trigSetOriginalValues]
ON [dbo].[Review]
FOR INSERT, UPDATE
AS
BEGIN
IF (NOT UPDATE(TemplateID) AND NOT UPDATE(ClientID)) return
DECLARE #TemplateID int
DECLARE #OriginalTemplateID int
DECLARE #ClientID int
DECLARE #OriginalClientID int
DECLARE #ReviewID int
SET #ReviewID = (SELECT ReviewID FROM inserted)
SET #ClientID = (SELECT ClientID FROM inserted)
SET #TemplateID = (SELECT TemplateID FROM inserted)
SET #OriginalTemplateID = (SELECT OriginalTemplateID FROM inserted);
SET #OriginalClientID = (SELECT OriginalClientID FROM inserted);
IF (#OriginalTemplateID IS NULL AND #TemplateID IS NOT NULL)
BEGIN
UPDATE [dbo].[Review] SET OriginalTemplateID = #TemplateID WHERE ReviewID=#ReviewID
END
IF (#OriginalClientID IS NULL AND #ClientID IS NOT NULL)
BEGIN
UPDATE [dbo].[Review] SET OriginalClientID = #ClientID WHERE ReviewID=#ReviewID
END
END
This should be your trigger:
UPDATE A
SET A.OriginalTemplateID = B.TemplateID
FROM [dbo].[Review] A
INNER JOIN INSERTED B
ON A.ReviewID = B.ReviewID
WHERE A.OriginalTemplateID IS NULL AND B.TemplateID IS NOT NULL
UPDATE A
SET A.OriginalClientID = B.ClientID
FROM [dbo].[Review] A
INNER JOIN INSERTED B
ON A.ReviewID = B.ReviewID
WHERE A.OriginalClientID IS NULL AND B.ClientID IS NOT NULL
Though you could still do this on a single UPDATE, but with a more complicated filter.

SQL: Query timeout expired

I have a simple query for update table (30 columns and about 150 000 rows).
For example:
UPDATE tblSomeTable set F3 = #F3 where F1 = #F1
This query will affected about 2500 rows.
The tblSomeTable has a trigger:
ALTER TRIGGER [dbo].[trg_tblSomeTable]
ON [dbo].[tblSomeTable]
AFTER INSERT,DELETE,UPDATE
AS
BEGIN
declare #operationType nvarchar(1)
declare #createDate datetime
declare #UpdatedColumnsMask varbinary(500) = COLUMNS_UPDATED()
-- detect operation type
if not exists(select top 1 * from inserted)
begin
-- delete
SET #operationType = 'D'
SELECT #createDate = dbo.uf_DateWithCompTimeZone(CompanyId) FROM deleted
end
else if not exists(select top 1 * from deleted)
begin
-- insert
SET #operationType = 'I'
SELECT #createDate = dbo..uf_DateWithCompTimeZone(CompanyId) FROM inserted
end
else
begin
-- update
SET #operationType = 'U'
SELECT #createDate = dbo..uf_DateWithCompTimeZone(CompanyId) FROM inserted
end
-- log data to tmp table
INSERT INTO tbl1
SELECT
#createDate,
#operationType,
#status,
#updatedColumnsMask,
d.F1,
i.F1,
d.F2,
i.F2,
d.F3,
i.F3,
d.F4,
i.F4,
d.F5,
i.F5,
...
FROM (Select 1 as temp) t
LEFT JOIN inserted i on 1=1
LEFT JOIN deleted d on 1=1
END
And if I execute the update query I have a timeout.
How can I optimize a logic to avoid timeout?
Thank you.
This query:
SELECT *
FROM (
SELECT 1 AS temp
) t
LEFT JOIN
INSERTED i
ON 1 = 1
LEFT JOIN
DELETED d
ON 1 = 1
will yield 2500 ^ 2 = 6250000 records from a cartesian product of INSERTED and DELETED (that is all possible combinations of all records in both tables), which will be inserted into tbl1.
Is that what you wanted to do?
Most probably, you want to join the tables on their PRIMARY KEY:
INSERT
INTO tbl1
SELECT #createDate,
#operationType,
#status,
#updatedColumnsMask,
d.F1,
i.F1,
d.F2,
i.F2,
d.F3,
i.F3,
d.F4,
i.F4,
d.F5,
i.F5,
...
FROM INSERTED i
FULL JOIN
DELETED d
ON i.id = d.id
This will treat update to the PK as deleting a record and inserting another, with a new PK.
Thanks Quassnoi, It's a good idea with "FULL JOIN". It is helped me.
Also I try to update table in portions (1000 items in one time) to make my code works faster because for some companyId I need to update more than 160 000 rows.
Instead of old code:
UPDATE tblSomeTable set someVal = #someVal where companyId = #companyId
I use below one:
declare #rc integer = 0
declare #parts integer = 0
declare #index integer = 0
declare #portionSize int = 1000
-- select Ids for update
declare #tempIds table (id int)
insert into #tempIds
select id from tblSomeTable where companyId = #companyId
-- calculate amount of iterations
set #rc=##rowcount
set #parts = #rc / #portionSize + 1
-- update table in portions
WHILE (#parts > #index)
begin
UPDATE TOP (#portionSize) t
SET someVal = #someVal
FROM tblSomeTable t
JOIN #tempIds t1 on t1.id = t.id
WHERE companyId = #companyId
delete top (#portionSize) from #tempIds
set #index += 1
end
What do you think about this? Does it make sense? If yes, how to choose correct portion size?
Or simple update also good solution? I just want to avoid locks in the future.
Thanks