USE [ddb]
GO
SET ANSI_NULLS OFF
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TRIGGER [dbo].[requeststrigger]
ON [dbo].[requests]
AFTER INSERT,UPDATE
AS
BEGIN
DECLARE #email VARCHAR(400);
DECLARE #firstname VARCHAR(400);
DECLARE #requestno VARCHAR(400);
DECLARE #lastname VARCHAR(400);
DECLARE #statusid INT;
DECLARE thecursor CURSOR FOR SELECT inserted.requestno,contacts.firstname,contacts.lastname,contacts.email FROM request_contacts,contacts,inserted WHERE request_contacts.requestid=inserted.requestid AND contacts.contactid=request_contacts.contactid AND request_contacts.notification=1 AND contacts.notification=1;
SET #statusid = (SELECT statusid FROM inserted);
IF #statusid = 4 AND #statusid <> (SELECT statusid FROM deleted)
BEGIN
SET NOCOUNT ON
SET ARITHABORT ON
OPEN thecursor;
FETCH NEXT FROM thecursor
INTO #requestno,#firstname,#lastname,#email
WHILE ##FETCH_STATUS = 0
BEGIN
EXEC MAIL_SEND #email,#firstname,#requestno,#lastname;
FETCH NEXT FROM thecursor
INTO #requestno,#firstname,#lastname,#email
END
CLOSE thecursor;
DEALLOCATE thecursor
SET NOCOUNT OFF
END
END
This simply makes the whole UPDATE/INSERT not work. When I remove the cursor declaration, it works. The cursor is just selecting a field from a table that is existing in the same database called "contacts". What is wrong?
Are you prepared to consider amending your design? There appear to be a couple of issues with what you're attempting here.
A trigger isn't necessarily the best place to be doing this kind of row-by-row operation since it executes in-line with the changes to the source table, and will affect the performance of the system negatively.
Also, your existing code evaluates statusid only for a single row in the batch, although logically it could be set to more than one value in a single batch of updates.
A more robust approach might be to insert the rows which should generate a MAIL_SEND operation to a queuing table, from which a scheduled job can pick rows up and execute MAIL_SEND, setting a flag so that each operation is carried out only once.
This would simplify your trigger to an insert - no cursor would be required there (although you will still need a loop of some kind in the sechduled job).
Related
I have a merge statement supposed to execute a trigger multiple times.
I first thought my trigger wasn't executing, but with some research I found that triggers are only triggered once per statement (a trigger being one statement).
But all the posts out there are old and I thought that there might be a simple way now to make my trigger execute multiple times.
So is there anything I can add to my trigger or my merge statement to make my trigger do so?
Thanks
TRIGGER
TRIGGER [dbo].[Sofi_TERA_Trigger]
ON [dbo].[ZZ]
AFTER INSERT,UPDATE
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
IF EXISTS(SELECT 1 FROM inserted WHERE inserted.Statut LIKE '%CLOT%' OR inserted.Statut LIKE '%CLTT%' OR inserted.Statut LIKE '%CONF%')
BEGIN
DECLARE #Id int;
DECLARE #Matricule varchar(10);
DECLARE #IdAction int;
DECLARE #NumeroOF int;
SELECT #NumeroOF = inserted.Ordre from inserted;
DECLARE OF_CURSOR CURSOR
LOCAL STATIC READ_ONLY FORWARD_ONLY
FOR
SELECT Id,Log.Matricule,IdAction from Log inner join (select max(Id) as maxID,Matricule from LOG where Log.NumeroOF = #NumeroOF group by Matricule) maxID
on maxID.maxID = Log.Id where Log.NumeroOF = #NumeroOF;
OPEN OF_CURSOR
FETCH NEXT FROM OF_CURSOR INTO #Id,#Matricule,#IdAction
WHILE ##FETCH_STATUS = 0
BEGIN
IF #IdAction!=13
BEGIN
IF #IdAction<=2
BEGIN
insert into Log(NumeroOF,Matricule,IdAction,Date,EstAdmin) values (#NumeroOF,#Matricule,13,GETDATE(),1);
END
ELSE
BEGIN
insert into Log(NumeroOF,Matricule,IdAction,Date,EstAdmin) values (#NumeroOF,#Matricule,2,GETDATE(),1);
insert into Log(NumeroOF,Matricule,IdAction,Date,EstAdmin) values (#NumeroOF,#Matricule,13,GETDATE(),1);
END
END
FETCH NEXT FROM OF_CURSOR INTO #Id,#Matricule,#IdAction
END
CLOSE OF_CURSOR;
DEALLOCATE OF_CURSOR;
END
END
MERGE STATEMENT
Merge ZZ AS TARGET USING ZZTemp AS SOURCE
ON (Target.Operation=Source.Operation AND Target.Ordre=Source.Ordre)
WHEN MATCHED THEN
UPDATE SET TARGET.DateTERA=SOURCE.DateTERA, TARGET.MatTERA=SOURCE.MatTERA, TARGET.MatTERC=SOURCE.MatTERC
WHEN NOT MATCHED THEN
INSERT(Operation,Ordre,ElementOTP,Article,DesignationOF,PosteTravail,ValeurTemps,DHT,Statut,StatutOF,TexteActivite,DateTERA,MatTERA,MatTERC,StatutMat)
VALUES(SOURCE.Operation,SOURCE.Ordre,SOURCE.ElementOTP,SOURCE.Article,SOURCE.DesignationOF,SOURCE.PosteTravail,SOURCE.ValeurTemps,SOURCE.DHT,
SOURCE.Statut,SOURCE.StatutOF,SOURCE.TexteActivite,SOURCE.DateTERA,SOURCE.MatTERA,SOURCE.MatTERC,SOURCE.StatutMat);
Your problem is the cursor is incorrectly written to handle sets of data. Any trigger setting a value form inserted or deleted to a scalar variable is incorrect and for reasons of data integrity MUST be rewritten. This trigger is buggy. Period. There is no getting around that it must be rewritten (and any others that use the same technique).
The code inside your trigger should be something like:
INSERT INTO Log(NumeroOF,Matricule,IdAction,Date,EstAdmin)
SELECT max(Id),l.Matricule,l.IdAction, 13,GETDATE(),1
FROM Log l
JOIN Inserted i ON l.NumeroOF = i.Ordre
WHERE i.Statut LIKE '%CLOT%' OR i.Statut LIKE '%CLTT%' OR i.Statut LIKE '%CONF%'
GROUP BY l.Matricule,l.IdAction
INSERT INTO Log(NumeroOF,Matricule,IdAction,Date,EstAdmin)
SELECT max(Id),l.Matricule,l.IdAction, 2,GETDATE(),1
FROM Log l
JOIN Inserted i ON l.NumeroOF = i.Ordre
WHERE IdAction<=2
WHERE i.Statut LIKE '%CLOT%' OR i.Statut LIKE '%CLTT%' OR i.Statut LIKE '%CONF%'
GROUP BY l.Matricule,l.IdAction
Make sure to test with both single record and multiple record inserts as all triggers should be tested. Then try your MERGE once you are confident the trigger is correct.
I need some tip on tuning some TSQL to execute faster, it's taking way too long although it works. This may be because I'm fetching a key from another table before I can do the insert, any ideas anyone?
DECLARE db_cursorReads CURSOR FOR SELECT
[MeterId]
,[MeterRead]
FROM MdsReadsImports;
declare #PremiseMeterId int;
declare #MeterId nvarchar(24);
declare #MeterRead int;
OPEN db_cursorReads;
FETCH NEXT FROM db_cursorReads INTO
#MeterId
,#MeterRead;
WHILE ##FETCH_STATUS = 0
BEGIN
set #PremiseMeterId = (select top 1 PremiseMeterId from PremiseMeters where MeterId = #MeterId)
insert into PremiseMeterReads (MeterRead,PremiseMeterId)
values (#MeterRead, #MPremiseMeterId)
FETCH NEXT FROM db_cursorReads INTO
#MeterId
,#MeterRead;
END;
CLOSE db_cursorReads;
DEALLOCATE db_cursorReads;
I see you are retrieving PremiseMeterId but not using it in the script you posted. Perhaps you can ditch the cursor and perform a single set-based query:
INSERT INTO PremiseMeterReads (MeterRead,MeterId)
SELECT
[MeterRead]
,[MeterId]
FROM MdsReadsImports;
First, I note that you are setting but not using #PremiseMeterID.
Second, you seem to be doing this:
insert into PremiseMeterReads (MeterRead, MeterId)
select MeterRead, MeterId
from MdsReadsImports;
A set based operation should be much faster than using a cursor.
I created a simple trigger:
ALTER TRIGGER [dbo].[idlist_update] ON [dbo].[Store]
FOR INSERT, UPDATE
AS
BEGIN
DECLARE #brand varchar(50);
DECLARE #model varchar(50);
DECLARE #category varchar(100);
DECLARE #part varchar(100);
DECLARE #count int;
SELECT #count = COUNT(*) FROM inserted;
SELECT #brand=Brand, #model=Model, #category=AClass, #part=Descript FROM inserted;
EXECUTE GenerateId_Part #brand, #model, #category, #part;
END
With rows, modified by our users (they using special application), it works ok, but I need to apply it to all rows in the table (more than 200.000+). I tried:
UPDATE Store SET lastupd={fn NOW()};
But it does not work.
I believe the syntax you want is:
UPDATE dbo.Store SET lastupd = CURRENT_TIMESTAMP;
However if you are updating the entire table every time you insert or update a single row, this seems quite wasteful to me. Why not store that fact once, somewhere else, instead of storing it redundantly 200,000 times? It seems to be a property of the store itself, not the products in it.
Also note that your trigger won't properly handle multi-row operations. You can't do this "assign a variable from inserted" trick because if two rows are inserted by a single statement, you'll only affect one arbitrary row. Unlike some platforms, in SQL Server triggers fire per statement, not per row. We can help fix this if you show what the stored procedure GenerateId_Part does.
Goal
I need to alter a number of almost identical triggers on a number of tables (and a number of databases).
Therefore I wan't to make one big script, and perform all the changes in one succeed-or-fail transaction.
My first attempt (that doesn't work)
---First alter trigger
ALTER TRIGGER [dbo].[trg_UserGarbleValue] ON [dbo].[users]
FOR INSERT
AS
Begin
DECLARE #GarbleValue NVARCHAR(200)
DECLARE #NewID NVARCHAR(20)
SET #NewID = (SELECT TOP 1 usr_id FROM users ORDER BY usr_id DESC)
SET #GarbleValue = dbo.fn_GetRandomString(4) + #NewID + dbo.fn_GetRandomString(4)
UPDATE users SET usr_garble_value = #GarbleValue WHERE usr_id = #NewID
End
Go
--Subsequent alter trigger (there would be many more in the real-world usage)
ALTER TRIGGER [dbo].[trg_SegmentGarbleValue] ON [dbo].[segment]
FOR INSERT
AS
Begin
DECLARE #GarbleValue NVARCHAR(200)
DECLARE #NewID NVARCHAR(20)
SET #NewID = (SELECT TOP 1 seg_id FROM segment ORDER BY seg_id DESC)
SET #GarbleValue = dbo.fn_GetRandomString(4) + #NewID + dbo.fn_GetRandomString(4)
UPDATE segment SET seg_garble_value = #GarbleValue WHERE seg_id = #NewID
End
Go
Running each of the alter trigger statements by themselves works fine. But when both of them are run in the same transaction, the declares crash in the second alter because the variables name already exists.
How do I accomplish this? Is there any way to declare a variable locally within a begin-end scope, or do I need to rethink it completely?
(I'm aware that the "top 1" for fetching new records is probably not very clever, but that is another matter)
I think you've confused GO (the batch separator) and transactions. It shouldn't complain about the variable names being redeclared, provided the batch separators are still present:
BEGIN TRANSACTION
GO
ALTER TRIGGER [dbo].[trg_UserGarbleValue] ON [dbo].[users]
FOR INSERT
AS
---Etc
Go
ALTER TRIGGER [dbo].[trg_SegmentGarbleValue] ON [dbo].[segment]
FOR INSERT
AS
---Etc
Go
COMMIT
As to your note about TOP 1, it's worse than you think - a trigger runs once per statement, not once per row - it could be running in response to multiple rows having been inserted. And, happily, there is a pseudo-table available (called inserted) that contains exactly those rows which caused the trigger to fire - there's no need for you to go searching for those row(s) in the base table.
I tried to achieve row level delete trigger by using cursor but when in trying yo delete the any row from table it tooks so long time.
I could not understand where exactly it stuck.
/****** Object: Trigger [delStudent] Script Date: 06/24/2010 12:33:33 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TRIGGER [delStudent]
ON [dbo].[Student]
FOR DELETE
AS
DECLARE #Roll as varChar(50);
DECLARE #Name as varChar(50);
DECLARE #Age as int;
DECLARE #UserName as varChar(50);
SELECT #UserName=SYSTEM_USER;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN TRANSACTION;
declare CurD cursor for select roll, Sname, age from deleted
open CurD
WHILE ##FETCH_STATUS = 0
BEGIN
INSERT INTO [dbo].[Audit]
(roll,sname,age,userId)
VALUES
(#Roll,#Name,#Age,#UserName)
END
COMMIT TRANSACTION;
Close CurD
DEALLOCATE CurD
I think you should transform your cursor in an insert-select sentence. I'm not sure this will solve your problem, but it's a good best practice anyway.
INSERT [dbo].[Audit] (roll,sname,age,userId)
SELECT 'FIELDS FROM DELETED', SYSTEM_USER
FROM deleted
Try to avoid cursors, and this will result in better performance.