infinite trigger loop....by design(!). How to work-around? - sql

I know I'm going to get flamed for this, but....
I have table ProductA, ProductB, and ProductC which have very similar schema but for 2 or 3 columns in each. Each table has an insert trigger which fires a duplicate row for each insert in A, B, or C to table Products, which is a consolidation of all products. In addition, update triggers on A,B, or C will likewise update their equivalent row in Table Products, as do delete triggers. All working flawlessly until.....we update, say, Table Products Column A, which also exists in Table A, B, and C.
I'm looking to develop a trigger on Table Products that will propogate that update in Column A to Column A in each of tables A, B, and C, BUT, without invoking the update triggers on Tables A, B, and C. The desired behavior is for updates to work in both directions without incurring an endless loop.(Note, only 2 columns in table products need to be replicated BACK to tables A, B, and C)
Options are:
redesign the schema so this situation doesn't exist (not in the
cards, this is a quick solution, redesign can be done by someone
else);
Manually disable the triggers when I update table products
(this is all done at the application level, users won't have the
ability to log into SSMA and disable triggers when they update table
products);
Come to Stack Overflow and hope someone has already encountered this type of problem!
Conceptually, how could this be done?
6/7 Update:
Here is the trigger code on Table A (e.g):
ALTER TRIGGER [dbo].[GRSM_WETLANDS_Point_GIS_tbl_locations_update]
ON [dbo].[GRSM_WETLANDS_POINT]
after update
AS
BEGIN
SET NOCOUNT ON;
update dbo.TBL_LOCATIONS
set
X_Coord = i.X_Coord,
Y_Coord = i.Y_Coord,
PlaceName = i.PlaceName,
FCSubtype = case
when i.FCSubtype = 1 then 'Point: Too Small to Determin Boundary'
when i.FCSubtype = 2 then 'Point: Boundary Determined by Contractor but not Surveyed'
when i.FCSubtype = 3 then 'Point: Wetland Reported but not yet Surveyed'
end ,
Landform = i.Landform
from dbo.TBL_LOCATIONS
Join inserted i
on TBL_LOCATIONS.GIS_Location_ID = i.GIS_Location_ID
end
GO
And
ALTER TRIGGER [dbo].[GRSM_WETLANDS_POINT_GIS_tbl_locations]
ON
[dbo].[GRSM_WETLANDS_POINT]
after INSERT
AS
BEGIN
SET NOCOUNT ON;
INSERT dbo.TBL_LOCATIONS(
X_Coord, Y_Coord,
PlaceName,
FCSubtype, Landform
)
SELECT
a.X_Coord, a.Y_Coord,
a.PlaceName,
a.FCSubtype, a.Landform
From
(
SELECT
X_Coord, Y_Coord,
PlaceName,
FCSubtype = case
when FCSubtype = 1 then 'Point: Too Small to Determin Boundary'
when FCSubtype = 2 then 'Point: Boundary Determined by Contractor but not Surveyed'
when FCSubtype = 3 then 'Point: Wetland Reported but not yet Surveyed'
end ,
Landform
FROM inserted
) AS a
end
GO
And here is the currently disabled update trigger on table products:
ALTER TRIGGER [dbo].[tbl_locations_updateto_geo]
ON [dbo].[TBL_LOCATIONS]
for update
AS
BEGIN
--IF ##NESTLEVEL>1 RETURN
SET NOCOUNT ON;
update dbo.GRSM_Wetlands_Point
set
X_Coord = i.X_Coord,
Y_Coord = i.Y_Coord,
PlaceName = i.PlaceName,
FCSubtype = i.FCSubtype,
Landform = i.Landform,
from dbo.TBL_LOCATIONS
Join inserted i
on TBL_LOCATIONS.GIS_Location_ID = i.GIS_Location_ID
where TBL_LOCATIONS.FCSubtype = 'Polygon: Determination Made by GPS Survey'
or TBL_LOCATIONS.FCSubtype = 'Polygon: Determination Derived from NWI'
or TBL_LOCATIONS.FCSubtype = 'Polygon: Determination Made by Other Means'
or TBL_LOCATIONS.FCSubtype = 'Polygon: Legal Jurisdictional Determination';
end
GO
(tbl names changed to keep with the posting text)

There are two types of recursion, direct and indirect: http://msdn.microsoft.com/en-us/library/ms190739.aspx
You can use the RECURSIVE_TRIGGERS option to stop direct recursion, but your case is indirect recursion so you'd have to set the nested triggers option. This will fix your problem, but if anything else in the system relies on recursion then it won't be a good option.
USE DatabaseName
GO
EXEC sp_configure 'show advanced options', 1
GO
RECONFIGURE
GO
EXEC sp_configure 'nested triggers', 0
GO
RECONFIGURE
GO
EDIT in response to your updated post:
I almost hate to give you this solution because you're ultimately taking a really crappy design and extending it... making even more of a mess than it is already instead of taking the time to understand what's going on and just fixing it. You should honestly just create another table to hold the values that need to be in sync between the two tables so the data is only in one place, and then relate those tables to that one through a key. But nonetheless...
You need a flag to set you're updating in one trigger so the other trigger can abort its operation if it sees it's true. Since (as far as I know) you can only have locally scoped variable, that means you'll need a table to store this flag value in and look it up from.
You can implement this solution with varying levels of complexity, but the easiest way is to just have all triggers set the flag to true when starting and false when ending. And before they start they check the flag and stop executing if it's true;
The problem with this is that there could be another update that isn't related to a trigger happening at the same time and it wouldn't get propogated to the next table. If you want to take this route, then I'll leave it up to you to figure out how to solve that problem.

Related

Trigger to update value with no of records

trying to set up a trigger but struggling to it to work the way i want, i want to update a field oppo_pono with the no of opportunity records created for a particular company record
so 1 company can have multiple opportunities and i want to record the no of master opportunities created for a company, so the first master opp created for a company would be set to 1 and so on
ive set the trigger up below but its setting the oppo_pono with the count from all companies rather then the one i am creating the opportunity for
my trigger below
USE [CRM]
GO
/****** Object: Trigger [dbo].[GeneratePNo] Script Date: 1/7/2021 3:55:27 PM ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
ALTER TRIGGER [dbo].[GeneratePNo]
ON [dbo].[Opportunity]
FOR insert
AS
declare #OppPrimary Int
declare #company Int
declare #compid Int
declare #type nvarchar(40)
declare #childopp nchar(1)
declare #pono int
Select #OppPrimary = Oppo_OpportunityId,
#company = Oppo_PrimaryCompanyId,
#compid = comp_companyid,
#type = Oppo_Type,
#childopp = oppo_childoppo,
#pono = oppo_pono
FROM Inserted inner join company on Oppo_PrimaryCompanyId = #compid
Begin
UPDATE [Opportunity] SET oppo_pono = (select count(*) from vSearchListOpportunity where Oppo_Deleted is null and #type = 'Master' and Oppo_PrimaryCompanyId = ) +1
WHERE Oppo_OpportunityId =#OppPrimary
End
As mentioned in the comments, you have not taken into account the inserted pseudo-table having multiple rows. You also have a number of outright syntax errors.
EDIT: Following your comments I think I understand what you are trying to do. My original solution will not work here because indexed view cannot have ranking functions, but I have modified it to work with what you need.
Ideally, you wouldn't care about the actual IDs lining up, and just use an IDENTITY column, but often an ID series per group is needed.
Generally a view with correct indexing will be the more performant option, but it depends what you need.
Using a View
I will show you a solution that can be used for a lot of different types of aggregations which normally require triggers. This only works for your problem if you intend to have the numbering change if a row gets deleted out the middle of the grouping. If you want the numbering to remain then use a trigger instead
I am unsure the exact relation of Opportunity, Company and vSearchListOpportunity (seems to be a view on Opportunity) but you should be able to modify this to suit.
Create an indexed view on the data, and include a row number for each row:
CREATE VIEW vOpportunityNumbered
AS
SELECT
o.Oppo_OpportunityId,
o.Oppo_PrimaryCompanyId,
o.Oppo_Type,
o.oppo_childoppo,
ROW_NUMBER() OVER
(PARTITION BY o.Oppo_PrimaryCompanyId ORDER BY o.Oppo_OpportunityId)
-- Order by primary key to get deterministic ordering
FROM Opportunity AS o
WHERE o.Oppo_Deleted IS NULL;
GO
Now, to support this view, we cannot index it directly, as mentioned. We can, however, create an index on the base table that will support it:
CREATE UNIQUE CLUSTERED INDEX opp_CompanyOpportunity
ON Opportunity (Oppo_PrimaryCompanyId, Oppo_OpportunityId)
-- note the ordering of the columns
INCLUDE (Oppo_Type, oppo_childoppo)
WITH (OPTIMIZE_FOR_SEQUENTIAL_KEY = ON) -- ONLY FOR SQL2019
;
GO
This view will now give you a sequential row numbering of Opportunity for each distinct Company.
Triggers
If you wish for the IDs to always remain the same no matter what happens to intervening rows, you will need a trigger (i.e. a deleted row will leave a gap in the numbers).
Every trigger has two tables, inserted and deleted, which contain the data that was changed. For update triggers, both tables have data, a row in each for each changed row.
This means that the trigger is executed once per statement, and these tables contain all the relevant rows. You cannot, however, update them directly; you must join the real tables to them.
So let's take a look at how to write a trigger. Again, I'm somewhat guessing as to the relations of the tables:
CREATE OR ALTER TRIGGER [dbo].[GeneratePNo]
ON [dbo].[Opportunity]
AFTER INSERT -- FOR is an alternative syntax, AFTER is more usual
AS
-- No need for BEGIN and END, the whole batch until GO is the trigger
SET NOCOUNT ON; -- Prevent DONE_IN_PROC rowcount messages
IF (NOT EXISTS (SELECT 1 FROM inserted))
RETURN; -- Bail-out early if no rows
-- We do not declare variables because we cannot store multiple rows in variables
UPDATE
o
SET oppo_pono =
ISNULL( --If there are no other rows we would get a null
(SELECT MAX(allO.oppo_pono)
FROM Opportunity allO
-- no need for the following two filters as the oppo_pono needs to be unique anyway
-- where allO.Oppo_Deleted is null and allO.type = 'Master'
WHERE allO.Oppo_PrimaryCompanyId = inserted.Oppo_PrimaryCompanyId
), 0) + 1
FROM inserted i
JOIN Opportunity o ON o.Oppo_OpportunityId = i.Oppo_OpportunityId;
-- We join inserted table on primary key always
GO
There are more efficient ways to write that update, but it depends whether you are inserting a lot of rows. An INSTEAD OF trigger will also be more performant here, but I haven't attempted that as I don't have your table definition.

SQL Server : make update trigger don't activate with no changing value

I want to track the update changes in a table via a trigger:
CREATE TABLE dbo.TrackTable(...columns same as target table)
GO
CREATE TRIGGER dboTrackTable
ON dbo.TargetTable
AFTER UPDATE
AS
INSERT INTO dbo.TrackTable (...columns)
SELECT (...columns)
FROM Inserted
However in real production some of the update queries select rows with vague conditions and update them all regardless of whether they are actually changed, like
UPDATE Targettable
SET customer_type = 'VIP'
WHERE 1 = 1
--or is_obsolete = 0 or register_date < '20160101' something
But due to table size and to analyze, I only want to choose those actually modified data for tracking. How to achieve this goal?
My track table has many columns (so I do not prefer checking inserted and deleted column one by one) but it seldom changes structure.
I guess the following code will be useful.
CREATE TABLE dbo.TrackTable(...columns same as target table)
GO
CREATE TRIGGER dboTrackTable
ON dbo.TargetTable
AFTER UPDATE
AS
INSERT INTO dbo.TrackTable (...columns)
SELECT *
FROM Inserted
EXCEPT
SELECT *
FROM Deleted
I realize this post is a couple months old now, but for anyone looking for a well-rounded answer:
To exit the trigger if no rows were affected on SQL Server 2016 and up, Microsoft recommends using the built-in ROWCOUNT_BIG() function in the Optimizing DML Triggers section of the Create Trigger documentation.
Usage:
IF ROWCOUNT_BIG() = 0
RETURN;
To ensure you are excluding rows that were not changed, you'll need to do a compare of the inserted and deleted tables inside the trigger. Taking your example code:
INSERT INTO dbo.TrackTable (...columns)
SELECT (...columns)
FROM Inserted i
INNER JOIN deleted d
ON d.[SomePrimaryKeyCol]=i.[SomePrimaryKeyCol] AND
i.customer_type<>d.customer_type
Microsoft documentation and w3schools are great resources for learning how to leverage various types of queries and trigger best practices.
Prevent trigger from doing anything if no rows changed.
Writing-triggers-the-right-way
CREATE TRIGGER the_trigger on dbo.Data
after update
as
begin
if ##ROWCOUNT = 0
return
set nocount on
/* Some Code Here */
end
Get a list of rows that changed:
CREATE TRIGGER the_trigger on dbo.data
AFTER UPDATE
AS
SELECT * from inserted
Previous stack overflow on triggers
#anna - as per #Oded's answer, when an update is performed, the rows are in the deleted table with the old information, and the inserted table with the new information –

A trigger that inserts several rows instead of one

We have an issue with the following trigger. We would like to insert a row into the UPDATEPROCESSINFO table when there is no row with the new INSTANCEID and update it for the next ones.
But we were surprised to discover that sometimes we have multiple rows with the same INSTANCEID. Is it because it was very fast? How to prevent this from happening? Our aim is to have one row per INSTANCEID.
Thanks for help
create or replace TRIGGER TRIG_UPDATE_PROCESS_INFO
AFTER INSERT ON PROCESSSTEP
FOR EACH ROW
DECLARE
AUDIT_TIME TIMESTAMP(6);
BEGIN
SELECT MAX(LASTUPDATETIME)
INTO AUDIT_TIME
FROM UPDATEPROCESSINFO
WHERE INSTANCEID = :NEW.INSTANCEID;
IF AUDIT_TIME IS NULL THEN
INSERT INTO UPDATEPROCESSINFO
(INSTANCEID, STEPID, STEPSTATUS, STEPITERATION, LASTUPDATETIME)
VALUES
(:NEW.INSTANCEID, :NEW.STEPID, :NEW.STATUS, :NEW.STEPITERATION, :NEW.AUDITTIMESTAMP);
ELSIF :NEW.AUDITTIMESTAMP > AUDIT_TIME THEN
UPDATE UPDATEPROCESSINFO
SET STEPID = :NEW.STEPID,
LASTUPDATETIME = :NEW.AUDITTIMESTAMP,
STEPSTATUS = :NEW.STATUS,
STEPITERATION = :NEW.STEPITERATION
WHERE INSTANCEID = :NEW.INSTANCEID;
END IF;
END;
This may be occurring because you have multiple sessions which are inserting into PROCESSSTEP for the same INSTANCEID. If two of the sessions insert into PROCESSSTEP at nearly the same time, and neither of them has committed their changes, then neither session will "see" the other's changes, and both will think that a row does not exist in UPDATEPROCESSINFO.
In my view this design appears to have a problem. I suggest changing it to have a PROCESS_STEP_HISTORY table, and as each step in the process is completed a row is inserted into PROCESS_STEP_HISTORY to record the information for the process step that was completed. Then, when something needed to find out information about the "last" step which was completed it would just do something like
SELECT a.*
FROM (SELECT *
FROM PROCESS_STEP_HISTORY h
WHERE INSTANCE_ID = whatever
ORDER BY LASTUPDATETIME DESC) a
WHERE ROWNUM = 1
It also has the advantage of preserving information about every step in the process, which may prove useful.
I also don't recommend using a trigger to do this sort of thing. This is business logic, and putting business logic into triggers is never a good idea.
Best of luck.

Database trigger

I have no experience in writing database trigger but I need one in my current project.
My use case is the following. I have two tables - Table 1 and Table 2.
These tables have a 1 : m relation.
My usecase is, if all records in Table1 have "VALUE2" than value in Table2 should updated to VALUE2.
So if record-value with ID 3 of table1 is updated to VALUE2 than Value of table2 also should be updated to value2.
It would be great if someone could help me - Thanks a lo for than!
TABLE1:
ID FK_Table2 VALUE
-----------------------------
1 77 VALUE2
2 77 VALUE2
3 77 VALUE1
4 54 OTHERVALUE
TABLE2:
ID VALUE
---------------
77 VALUE1
So you need to learn and try basic trigger first.
CREATE OR REPLACE TRIGGER trigger_name
AFTER UPDATE ON TABLE1
FOR EACH ROW
BEGIN
/* trigger code goes here...*/
/* for this particular case you need to update value of table2 */
UPDATE TABLE2 SET VALUE = new.VALUE WHERE TABLE2.ID = new.FK_Table2 ;
END
Try and write some code. IF stucked... come back and let us know...
No matter which system, there are some basic rules or best practices you should know. One is that it is bad form (and outright prohibited in many systems) for a trigger to reach back out and query the very table the trigger is written for. Your use case requires the trigger on Table1 to go back out and read from Table1 during the Update operation. Not good.
One available option is to use a stored procedure to handle all the updates to this table. They are more awkward to work with (for example: if a parameter is NULL, does that mean put a NULL in the corresponding field or leave it unmodified?). For that reason, and with the understanding that this is based on the limited amount of information in the question, I would recommend one of two alternatives.
One is to have a stored procedure that is used only to change the VALUE field. That field is not changed in a vacuum, but as part of a larger process. The step in the process that actually ends up changing the field could then call the SP.
Another is to front the table with a view with an "instead of" trigger and perform all DML through the view. This is the method I prefer, at least on those systems that allow triggers on views. The view trigger may query the underlying table as needed.
As for the logic (SP or trigger) here is some pseudo code:
-- Make the update
update table1 set value = #somevalue
where id = #someid;
-- Get the group that id is in
select FK_Table2 into #somegroupid
from Table1
where id = #someid;
-- Are all the values in that group the same?
select count(*) into #OtherValues
from Table1
where FK_Table2 = #somegroupid
and value <> #somevalue;
-- If so, notify the other table.
if #OtherValues = 0 then
update table2 set value = #somevalue
where id = #somegroupid;
I hope this answers your immediate question. However, based on what you have shown us here, the major cause of the problem would seem to be poor design. Let us know the higher level requirement you are trying to fill here and I'll bet we could come up with some modeling changes that would make this a whole lot easier without having to get really clever with SPs or triggers.

How to check if a column is being updated in an INSTEAD OF UPDATE Trigger

I am making some tweaks to a legacy application built on SQL Server 2000, needless to say I only want to do the absolute minimum in the fear that it may just all fall apart.
I have a large table of users, tbUsers, with a BIT flag for IsDeleted. I want to archive off all current and future IsDeleted = 1 user records into my archive table tbDeletedUsers.
Moving the currently deleted users is straight forward, however I want a way to move any future users where the IsDeleted flag is set. I could use a standard AFTER trigger on the column however I plan to add some constraints to the tbUser table that would violate this, what I'd like is for my INSTEAD OF UPDATE trigger to fire and move the record to archive table instead?
I guess my question is... is it possible to trigger an INSTEAD OF UPDATE trigger on the update of an individual column? This is what I have so far:
CREATE TRIGGER trg_ArchiveUsers
INSTEAD OF UPDATE ON tbUsers
AS
BEGIN
...
END
GO
If so an example (SQL 2000 compatible) would be much appreciated!
Using the UPDATE(columnname) test, you can check in a trigger whether a specific column was updated (and then take specific actions), but you can't have a trigger fire only on the update of a specific column. It will fire as soon as the update is performed, regardless of the fact which column was the target of the update.
So, if you think you have to use an INSTEAD OF UPDATE trigger, you'll need to implement two kinds of actions in it:
1) insert into tbDeletedUsers + delete from tbUsers – when IsDeleted is updated (or, more exactly, updated and set to 1);
2) update tbUsers normally – when IsDeleted is not updated (or updated but not set to 1).
Because more than one row can be updated with a single UPDATE instruction, you might also need to take into account that some rows might have IsDeleted set to 1 and others not.
I'm not a big fan of INSTEAD OF triggers, but if I really had to use one for a task like yours, I might omit the UPDATE() test and implement the trigger like this:
CREATE TRIGGER trg_ArchiveUsers
ON tbUsers
INSTEAD OF UPDATE
AS
BEGIN
UPDATE tbUsers
SET
column = INSERTED.column,
…
FROM INSERTED
WHERE INSERTED.key = tbUsers.key
AND INSERTED.IsDeleted = 0
;
DELETE FROM tbUsers
FROM INSERTED
WHERE INSERTED.key = tbUsers.key
AND INSERTED.IsDeleted = 1
;
INSERT INTO tbDeletedUsers (columns)
SELECT columns
FROM INSERTED
WHERE IsDeleted = 1
;
END