Trigger to update sum of columns? - sql

I know this is not a way to do it but it's a interview question
to update total = marks1 + marks2 + marks3 using a trigger.
I wrote something like this but it's not updating after an insert statement.
CREATE table marks
(
marks1 int,
marks2 int,
marks3 int,
total int
)
SELECT * from marks m
insert into marks values(10,10,20,0)
drop TRIGGER total_marks
create TRIGGER total_marks ON marks
AFTER INSERT
AS
begin
SET NOCOUNT ON
DECLARE #marks1 as int
select #marks1 = inserted.marks1 FROM inserted
DECLARE #marks2 as int
select #marks1 = inserted.marks2 FROM inserted
DECLARE #marks3 as int
select #marks1 = inserted.marks3 FROM inserted
DECLARE #result as int
set #result = #marks1 + #marks2 + #marks3
update marks
set total = #result
SET NOCOUNT OFF
end

Your trigger doesn't handle multiple row inserts, updates all rows to the same value (rather than just the row(s) inserted), and is far more complex than necessary anyway. Where is your key?
CREATE TRIGGER dbo.total_marks
ON dbo.marks
FOR INSERT
AS
BEGIN
SET NOCOUNT ON;
UPDATE m
SET total = i.marks1 + i.marks2 + i.marks3
FROM dbo.marks AS m
INNER JOIN inserted AS i
ON m.key = i.key;
END
GO
If your table really doesn't have a key (it doesn't make a whole lot of sense to me), then you can say this, but it may update rows that were already updated:
ON m.marks1 = i.marks1 AND m.marks2 = i.marks2 AND m.marks3 = i.marks3
WHERE m.total = 0;

Related

Trigger that prevents update of column based on result of the user defined function

We have DVD Rental company. In this particular scenario we consider only Member, Rental and Membership tables.
The task is to write a trigger that prevents a customer from being shipped a DVD
if they have reached their monthly limit for DVD rentals as per their membership contract using the function.
My trigger leads to infinite loop. It works without While loop, but then it does not work properly, if I consider multiple updates to the Rental table. Where I am wrong?
-- do not run, infinite loop
CREATE OR ALTER TRIGGER trg_Rental_StopDvdShip
ON RENTAL
FOR UPDATE
AS
BEGIN
DECLARE #MemberId INT
DECLARE #RentalId INT
SELECT * INTO #TempTable FROM inserted
WHILE (EXISTS (SELECT RentalId FROM #TempTable))
BEGIN
IF UPDATE(RentalShippedDate)
BEGIN
IF (SELECT TotalDvdLeft FROM dvd_numb_left(#MemberId)) <= 0
BEGIN
ROLLBACK
RAISERROR ('YOU HAVE REACHED MONTHLY LIMIT FOR DVD RENTALS', 16, 1)
END;
END;
DELETE FROM #TempTable WHERE RentalID = #RentalId
END;
END;
My function looks as follows:
CREATE OR ALTER FUNCTION dvd_numb_left(#member_id INT)
RETURNS #tab_dvd_numb_left TABLE(MemberId INT, Name VARCHAR(50), TotalDvdLeft INT, AtTimeDvdLeft INT)
AS
BEGIN
DECLARE #name VARCHAR(50)
DECLARE #dvd_total_left INT
DECLARE #dvd_at_time_left INT
DECLARE #dvd_limit INT
DECLARE #dvd_rented INT
DECLARE #dvd_at_time INT
DECLARE #dvd_on_rent INT
SET #dvd_limit = (SELECT Membership.MembershipLimitPerMonth FROM Membership
WHERE Membership.MembershipId = (SELECT Member.MembershipId FROM Member WHERE Member.MemberId = #member_id))
SET #dvd_rented = (SELECT COUNT(Rental.MemberId) FROM Rental
WHERE CONCAT(month(Rental.RentalShippedDate), '.', year(Rental.RentalShippedDate)) = CONCAT(month(GETDATE()), '.', year(GETDATE())) AND Rental.MemberId = #member_id)
SET #dvd_at_time = (SELECT Membership.DVDAtTime FROM Membership
WHERE Membership.MembershipId = (SELECT Member.MembershipId FROM Member WHERE Member.MemberId = #member_id))
SET #dvd_on_rent = (SELECT COUNT(Rental.MemberId) FROM Rental
WHERE Rental.MemberId = #member_id AND Rental.RentalReturnedDate IS NULL)
SET #name = (SELECT CONCAT(Member.MemberFirstName, ' ', Member.MemberLastName) FROM Member WHERE Member.MemberId = #member_id)
SET #dvd_total_left = #dvd_limit - #dvd_rented
SET #dvd_at_time_left = #dvd_at_time - #dvd_on_rent
IF #dvd_total_left < 0
BEGIN
SET #dvd_total_left = 0
SET #dvd_at_time_left = 0
INSERT INTO #tab_dvd_numb_left(MemberId, Name, TotalDvdLeft, AtTimeDvdLeft)
VALUES(#member_id, #name, #dvd_total_left, #dvd_at_time_left)
RETURN;
END
INSERT INTO #tab_dvd_numb_left(MemberId, Name, TotalDvdLeft, AtTimeDvdLeft)
VALUES(#member_id, #name, #dvd_total_left, #dvd_at_time_left)
RETURN;
END;
Will be glad for any advice.
Your main issue is that even though you populate #TempTable you never pull any values from it.
CREATE OR ALTER TRIGGER trg_Rental_StopDvdShip
ON RENTAL
FOR UPDATE
AS
BEGIN
DECLARE #MemberId INT, #RentalId INT;
-- Move test for column update to the first test as it applies to the entire update, not per row.
IF UPDATE(RentalShippedDate)
BEGIN
SELECT * INTO #TempTable FROM inserted;
WHILE (EXISTS (SELECT RentalId FROM #TempTable))
BEGIN
-- Actually pull some information from #TempTable - this wasn't happening before
SELECT TOP 1 #RentalID = RentalId, #MemberId = MemberId FROM #TempTable;
-- Select our values to its working
-- SELECT #RentalID, #MemberId;
IF (SELECT TotalDvdLeft FROM dvd_numb_left(#MemberId)) <= 0
BEGIN
ROLLBACK
RAISERROR ('YOU HAVE REACHED MONTHLY LIMIT FOR DVD RENTALS', 16, 1)
END;
-- Delete the current handled row
DELETE FROM #TempTable WHERE RentalID = #RentalId
END;
-- For neatness I always drop temp tables, makes testing easier also
DROP TABLE #TempTable;
END;
END;
An easy way to debug simply triggers like this is to copy the T-SQL out and then create an #Inserted table variable e.g.
DECLARE #Inserted table (RentalId INT, MemberId INT);
INSERT INTO #Inserted (RentalId, MemberId)
VALUES (1, 1), (2, 2);
DECLARE #MemberId INT, #RentalId INT;
-- Move test for column update to the first test as it applies to the entire update, not per row.
-- IF UPDATE(RentalShippedDate)
BEGIN
SELECT * INTO #TempTable FROM #inserted;
WHILE (EXISTS (SELECT RentalId FROM #TempTable))
BEGIN
-- Actually pull some information from #TempTable - this wasn't happening before
SELECT TOP 1 #RentalID = RentalId, #MemberId = MemberId FROM #TempTable;
-- Select our values to its working
SELECT #RentalID, #MemberId;
-- IF (SELECT TotalDvdLeft FROM dvd_numb_left(#MemberId)) <= 0
-- BEGIN
-- ROLLBACK
-- RAISERROR ('YOU HAVE REACHED MONTHLY LIMIT FOR DVD RENTALS', 16, 1)
-- END;
-- Delete the current handled row
DELETE FROM #TempTable WHERE RentalID = #RentalId
END;
-- For neatness I always drop temp tables, makes testing easier also
DROP TABLE #TempTable;
END;
Note: throw is the recommended way to throw an error instead of raiserror.
Another thing to consider is that you must try to transform your UDF into an inline TVF because of some side effects.
Like this one:
CREATE OR ALTER FUNCTION dvd_numb_left(#member_id INT)
RETURNS TABLE
AS
RETURN
(
WITH
TM AS
(SELECT Membership.MembershipLimitPerMonth AS dvd_limit,
Membership.DVDAtTime AS dvd_at_time,
CONCAT(Member.MemberFirstName, ' ', Member.MemberLastName) AS [name]
FROM Membership AS MS
JOIN Member AS M
ON MS.MembershipId = M.MembershipId
WHERE M.MemberId = #member_id
),
TR AS
(SELECT COUNT(Rental.MemberId) AS dvd_rented
FROM Rental
WHERE YEAR(Rental.RentalShippedDate ) = YEAR(GETDATE)
AND MONTH(Rental.RentalShippedDate ) = MONTH(GETDATE)
AND Rental.MemberId = #member_id
)
SELECT MemberId, [Name],
CASE WHEN dvd_limit - dvd_rented < 0 THEN 0 ELSE dvd_limit - dvd_rented END AS TotalDvdLeft,
CASE WHEN dvd_limit - dvd_rented < 0 THEN 0 ELSE dvd_at_time - dvd_on_rent END AS AtTimeDvdLeft
FROM TM CROSS JOIN TR
);
GO
Which will be much more efficient.
The absolute rule to have performances is: TRY TO STAY IN A "SET BASED" CODE instead of iterative code.
The above function can be optimized by the optimzer whilet yours cannot and will needs 4 access to the same tables.

How to sum multiple rows on trigger update?

I wrote a trigger on update on table [CART] to count and insert value to other table [HEADERS] to column [SUM]. It works but only for 1 row where [CART].[NUMBER] = [HEADERS].[NUMBER]. Column [NUMBER] in [CART] is not unique and i want to count all values from [CART] where [NUMBER] is identical. It means [AMOUNT]*[PRICE] for each row with same number, add this and insert into column [HEADERS].[SUM]
Here is what i got:
CREATE TRIGGER [dbo].[sum] ON [dbo].[CART] AFTER UPDATE AS
BEGIN
DECLARE #RESULT DECIMAL
DECLARE #AMOUNT FLOAT
DECLARE #PRICE FLOAT
DECLARE #NUMBER FLOAT
SET NOCOUNT ON;
IF UPDATE([NUMBER]) or UPDATE([AMOUNT]) or UPDATE([STATUS])
BEGIN
SELECT #AMOUNT=[AMOUNT],#PRICE=[PRICE],#NUMBER=[NUMBER] FROM inserted
IF #NUMBER is not NULL
BEGIN
SELECT #RESULT=#AMOUNT * #PRICE
UPDATE HEADERS SET SUM=#RESULT WHERE NUMBER=#NUMBER
END
END
GO
I'm not really good at SQL and i cant find correct syntax for this task. Can you help me?
Your trigger code does not handle cases when multiple rows are updated.
CREATE TRIGGER [dbo].[sum] ON [dbo].[CART] AFTER UPDATE AS
BEGIN
SET NOCOUNT ON;
IF UPDATE([NUMBER]) or UPDATE([AMOUNT]) or UPDATE([STATUS])
BEGIN
UPDATE H
SET [SUM] = I.AMOUNT * I.PRICE
FROM inserted I
INNER JOIN HEADER H ON I.NUMBER = H.NUMBER
END
GO

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

Can I put an INSERT inside an INSTEAD OF INSERT trigger in SQL Server?

I'm having a some troubles with a complicated query, that required me to insert something in a table, but if I found that two columns are the same I should stop the transaction with a trigger. I make some code to do that, but I'm not sure 100% of it even when it works fine now.
alter trigger TR1
on Passer instead of insert
as
begin
declare #A int
declare #B int
declare #C int
set #A = (select code_ligne from inserted)
set #B = (select ordre_passage from inserted)
set #C = (select code_ville from inserted)
select * from passer
where code_ligne = #A
and ordre_passage = #B
if(##rowcount = 0 )
begin
insert into Passer values(#A,#C,#B)
print 'okay'
print ##rowcount
end
end
When you have scalar variables like this in a trigger you are going to run into problems. If you have two rows inserted at once you will only get 1 row inserted into Passer. You don't need these variables at all. Just switch this around to be a single set based insert statement. Something along these lines.
alter trigger TR1 on Passer instead of insert as
begin
insert into Passer
(
code_ligne
, ordre_passage
, code_ville
)
select i.code_ligne
, i.ordre_passage
, i.code_ville
from inserted i
join Passer p on p.code_ligne = i.code_ligne
and p.ordre_passage = i.ordre_passage
if(##rowcount = 0 ) begin
print 'okay'
print ##rowcount
end
end

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