I have created an insert/update trigger that is designed to update information in a different table based on the data being inserted. The last thing the trigger does (or is supposed to do) is remove all data from a target table with conditions that may have changed during the insert portions of the trigger.
Everything appears to be working with the trigger except the final DELETE statement. It is executing the DELETE statement, but not following any of the conditions in the where clause. It simply deletes everything in the table.
I have even tried changing the NOT IN in the where clause to IN, and it still does the same. I have isolated the DELETE statement and tested outside the trigger and it works fine (using the same variables and subqueries).
Am I missing something with the behavior of a trigger?
Here comes the code:
ALTER TRIGGER [dbo].[cust_trgr_profile_attribute]
ON [dbo].[port_module_instance_setting]
AFTER INSERT, UPDATE
AS
DECLARE #ModuleId INT=449,
#MatchGroupModSetting VARCHAR(50) = 'AttributeGroup',
#FilterGroupModSetting VARCHAR(50) = 'FilterAttributeGroup',
#MatchAttributes TABLE (attribute_id INT),
#FilterAttributes TABLE (attribute_id INT)
INSERT INTO #MatchAttributes
SELECT DISTINCT camatch.attribute_id
FROM inserted I
JOIN core_attribute camatch ON I.value = CONVERT(VARCHAR(10), camatch.attribute_group_id)
JOIN port_module_instance pmi ON I.module_instance_id = pmi.module_instance_id
AND pmi.module_id=#ModuleId
WHERE I.name like #MatchGroupModSetting+'_'
INSERT INTO #FilterAttributes
SELECT DISTINCT cafilter.attribute_id
FROM inserted I
JOIN core_attribute cafilter ON I.value = CONVERT(VARCHAR(10), cafilter.attribute_group_id)
JOIN port_module_instance pmi ON I.module_instance_id = pmi.module_instance_id
AND pmi.module_id=#ModuleId
WHERE I.name=#FilterGroupModSetting
IF ((SELECT COUNT(*) FROM #MatchAttributes) > 0 OR (SELECT COUNT(*) FROM #FilterAttributes) > 0)
BEGIN
IF (SELECT COUNT(*) FROM #MatchAttributes) > 0
BEGIN
UPDATE cpa
SET cpa.[required]=0
FROM cust_profile_attribute cpa
JOIN #MatchAttributes ma ON cpa.attribute_id = ma.attribute_id
END
IF (SELECT COUNT(*) FROM #FilterAttributes) > 0
BEGIN
UPDATE cpa
SET cpa.[required]=0
FROM cust_profile_attribute cpa
JOIN #FilterAttributes fa ON cpa.attribute_id=fa.attribute_id
END
DELETE FROM cust_profile_attribute
WHERE attribute_id NOT IN (SELECT distinct ca.attribute_id
FROM core_attribute ca
JOIN port_module_instance_setting inst ON CONVERT(VARCHAR(10),ca.attribute_group_id) = inst.value
JOIN port_module_instance modinst ON inst.module_instance_id = modinst.module_instance_id
AND modinst.module_id = #ModuleId
WHERE inst.name like #MatchGroupModSetting + '_'
OR inst.name like #FilterGroupModSetting)
END
I have found a flaw in my fundamental logic of how the trigger works. I have now refined my understanding of what is going on and able to articulate more information for others to help. Rather than try to completely transform the original post, I thought it better to post the new info here.
The basic idea is that the port_module_instance_setting table stores a string in the that represents a setting (in this specific case, the conditions are set so that the value field will always be a number). What I am trying to accomplish is that when the value field is updated in one of these specific "settings", everything in the cust_profile_attribute table that is referenced by the old value gets deleted. In this context, the value field of port_module_instance_setting is a numeric varchar value that directly references the attribute_group_id of core_attribute. Please don't comment on best practices regarding referencing tables using different data types, as I have no control over the actual table structure :)
Everything in the trigger functions properly, except the DELETE statement at the end. It isn't doing anything. Any ideas on why it isn't deleting the attributes I want it to?
I have changed the DELETE statement as follows to reference the pre-updated valuefield.
DELETE FROM cust_profile_attribute
WHERE attribute_id IN(SELECT ISNULL(attribute_id,-1) FROM deleted d
JOIN core_attribute ca ON ca.attribute_group_id= CONVERT(INT,d.value))
Related
A follow up question to SQL Server Merge: update only changed data, tracking changes?
we have been struggling to get an effective merge statement working, and are now thinking about only using updates, we have a very simple problem: Update Target from Source where values are different and record the changes, both tables are the same layout.
So, the two questions we have are: is it possible to combine this very simple update into a single statement?
UPDATE tbladsgroups
SET tbladsgroups.Description = s.Description,
tbladsgroups.action='Updated'
FROM tbladsgroups t
INNER JOIN tbladsgroups_staging s
ON t.SID = s.SID
Where s.Description <> t.Description
UPDATE tbladsgroups
SET tbladsgroups.DisplayName = s.DisplayName,
tbladsgroups.action='Updated'
FROM tbladsgroups t
INNER JOIN tbladsgroups_staging s
ON t.SID = s.SID
Where s.DisplayName <> t.DisplayName
....for each column.
Second question.
Can we record into a separate table/variable which record has been updated?
Merge would be perfect, however we cannot see which record is updated as the data returned from OUTPUT shows all rows, as the target is always updated.
edit complete merge:
M
ERGE tblADSGroups AS TARGET
USING tblADSGroups_STAGING AS SOURCE
ON (TARGET.[SID] = SOURCE.[SID])
WHEN MATCHED
THEN UPDATE SET
TARGET.[Description]=CASE
WHEN source.[Description] != target.[Description] THEN(source.[Description]
)
ELSE target.[Description] END,
TARGET.[displayname] = CASE
WHEN source.[displayname] != target.[displayname] THEN source.[displayname]
ELSE target.[displayname] END
...other columns cut for brevity
WHEN NOT MATCHED BY TARGET
THEN
INSERT (
[SID],[SamAccountName],[DisplayName],[Description],[DistinguishedName],[GroupCategory],[GroupScope],[Created],[Members],[MemberOf],[SYNCtimestamp],[Action]
)
VALUES (
source.[SID],[SamAccountName],[DisplayName],[Description],[DistinguishedName],[GroupCategory],[GroupScope],[Created],[Members],[MemberOf],[SYNCtimestamp],[Action]
)
WHEN NOT MATCHED BY SOURCE
THEN
UPDATE SET ACTION='Deleted'
You can use a single UPDATE with an OUTPUT clause, and use an INTERSECT or EXCEPT subquery in the join clause to check whether any columns have changed.
For example
UPDATE t
SET Description = s.Description,
DisplayName = s.DisplayName,
action = 'Updated'
OUTPUT inserted.ID, inserted.Description, inserted.DisplayName
INTO #tbl (ID, Description, DisplayName)
FROM tbladsgroups t
INNER JOIN tbladsgroups_staging s
ON t.SID = s.SID
AND NOT EXISTS (
SELECT s.Description, s.DisplayName
INTERSECT
SELECT t.Description, t.DisplayName
);
You can do a similar thing with MERGE, if you also want to INSERT
MERGE tbladsgroups t
USING tbladsgroups_staging s
ON t.SID = s.SID
WHEN MATCHED AND NOT EXISTS ( -- do NOT place this condition in the ON
SELECT s.Description, s.DisplayName
INTERSECT
SELECT t.Description, t.DisplayName
)
THEN UPDATE SET
Description = s.Description,
DisplayName = s.DisplayName,
action = 'Updated'
WHEN NOT MATCHED
THEN INSERT (ID, Description, DisplayName)
VALUES (s.ID, s.Description, s.DisplayName)
OUTPUT inserted.ID, inserted.Description, inserted.DisplayName
INTO #tbl (ID, Description, DisplayName)
;
We have similar needs when dealing with values in our Data Warehouse dimensions. Merge works fine, but can be inefficient for large tables. Your method would work, but also seems fairly inefficient in that you would have individual updates for every column. One way to shorten things would be to compare multiple columns in one statement (which obviously makes things more complex). You also do not seem to take NULL values into consideration.
What we ended up using is essentially the technique described on this page: https://sqlsunday.com/2016/07/14/comparing-nullable-columns/
Using INTERSECT allows you to easily (and quickly) compare differences between our staging and our dimension table, without having to explicitly write a comparison for each individual column.
To answer your second question, the technique above would not enable you to catch which column changed. However, you can compare the old row vs the new row (we "close" the earlier version of the row by setting a "ValidTo" date, and then add the new row with a "ValidFrom" date equal to today's date.
Our code ends up looking like the following:
INSERT all rows from the stage table that do not have a matching key value in the new table (new rows)
Compare stage vs dimension using the INTERSECT and store all matches in a table variable
Using the table variable, "close" all matching rows in the Dimension
Using the table variable, INSERT the new rows
If there's a full load taking place, we can also check for Keys that only exist in the dimension but not in the stage table. This would indicate those rows were deleted in the source system, and we mark them as "IsDeleted" in the dimension.
I think you may be overthinking the complexity, but yes. Your underlying update is a compare between the ads group and staging tables based on the matching ID in each query. Since you are already checking the join on ID and comparing for different description OR display name, just update both fields. Why?
groups description groups display staging description staging display
SomeValue Show Me SOME other Value Show Me
Try This Attempt Try This Working on it
Both Diff Changes Both Are Diff Change Me
So the ultimate value you want is to pull both description and display FROM the staging back to the ads groups table.
In the above sample, I have three samples which if based on matching ID present entries that would need to be changed. If the value is the same in one column, but not the other and you update both columns, the net effect is the one bad column that get updated. The first would ultimately remain the same. If both are different, both get updated anyhow.
UPDATE tbladsgroups
SET tbladsgroups.Description = s.Description,
tbladsgroups.DisplayName = s.DisplayName,
tbladsgroups.action='Updated'
FROM tbladsgroups t
INNER JOIN tbladsgroups_staging s
ON t.SID = s.SID
Where s.Description <> t.Description
OR s.DisplayName <> t.DisplayName
Now, all this resolution being said, you have redundant data and that is the whole point of a lookup table. The staging appears to always have the correct display name and description. Your tblAdsGroups should probably remove those two columns and always get them from staging to begin with... Something like..
select
t.*,
s.Description,
s.DisplayName
from
tblAdsGroups t
JOIN tblAdsGroups_Staging s
on t.sid = s.sid
Then you always have the correct description and display name and dont have to keep synching updates between them.
I was trying to look for it online but couldn't find anything that will settle my doubts.
I want to figure out which one is better to use, when and why?
I know MERGE is usually used for an upsert, but there are some cases that a normal update with with subquery has to select twice from the table(one from a where clause).
E.G.:
MERGE INTO TableA s
USING (SELECT sd.dwh_key,sd.serial_number from TableA#to_devstg sd
where sd.dwh_key = s.dwh_key and sd.serial_number <> s.serial_number) t
ON(s.dwh_key = t.dwh_key)
WHEN MATCHED UPDATE SET s.serial_number = t.serial_number
In my case, i have to update a table with about 200mil records in one enviorment, based on the same table from another enviorment where change has happen on serial_number field. As you can see, it select onces from this huge table.
On the other hand, I can use an UPDATE STATEMENT like this:
UPDATE TableA s
SET s.serial_number = (SELECT t.serial_number
FROM TableA#to_Other t
WHERE t.dwh_serial_key = s.dwh_serial_key)
WHERE EXISTS (SELECT 1
FROM TableA#To_Other t
WHERE t.dwh_serial_key = s.dwh_serial_key
AND t.serial_number <> s.serial_number)
As you can see, this select from the huge table twice now. So, my question is, what is better? why?.. which cases one will be better than the other..
Thanks in advance.
I would first try to load all necessary data from remote DB to the temporary table and then work with that temporary table.
create global temporary table tmp_stage (
dwh_key <your_dwh_key_type#to_devstg>,
serial_number <your_serial_number_type##to_devstg>
) on commit preserve rows;
insert into tmp_stage
select dwh_key, serial_number
from TableA#to_devstg sd
where sd.dwh_key = s.dwh_key;
/* index (PK on dwh_key) your temporary table if necessary ...*/
update (select
src.dwh_key src_key,
tgt.dwh_key tgt_key,
src.serial_number src_serial_number,
tgt.serial_number tgt_serial_number
from tmp_stage src
join TableA tgt
on src.dwh_key = tgt.dwh_key
)
set src_serial_number = tgt_serial_number;
I am facing mutating trigger error,
I will describe the issue here
I am using tableA and tableB.
TableA holds a column called empChecked which can hold 2 values '-', '+'.
TableB holds a column called mgrChecked which can hold 2 values '-', '+'.
The current requirement is both fields in empChecked and mgrchecked must be in sync. i mean if empChecked is updated to '+' then mgr checked in tableB must be updated to '+' and vice versa. Updation is possible from front end for both fields.
I have created trigger on both the tables. but i am facing ora-04091 error.
Please suggest me any approach to achieve this?
The mutating table error is a code smell. It almost always points to a bad data model, usually insufficient normalisation.
Certainly a bad data model is in evidence here. You have a column on a table with two settings. That's fine. Now you want to add the same column to a second table and keep the two in sync. This new column is completely pointless. There is no information available in that new column than you cannot get from querying the first table.
And that's what ORA-04091 is telling you. You can spend an awful lot of time building a workaround but that would all be wasted effort.
Instead of using triggers for that kind of synchronization why don't you just create a view
named TableB that will "contain" all data from TableB (not mgrchecked) + empChecked column from TableA
Something like this
create or replace view TAbleB as
select t1.id
, t1.Column2
, ...
, t1.ColumnN
, t.empChecked
from TableA t
, TableB t1
where t.id = t1.id
You're a little unclear on detail, but I think this would help (assuming you have an ID column on both tables)
CREATE TRIGGER tri_table_a
AFTER UPDATE ON table_a
DECLARE
hits NUMBER :=0;
BEGIN
SELECT count(*) INTO hits FROM table_a a INNER JOIN table_b b ON (a.id=b.id) WHERE a.emp_checked<>b.mgr_checked;
IF hits>0 THEN
UPDATE table_b b SET mgr_checked=(SELECT emp_checked FROM table_a a WHERE a.id=b.id);
END IF;
END;
/
CREATE TRIGGER tri_table_b
AFTER UPDATE ON table_b
DECLARE
hits NUMBER :=0;
BEGIN
SELECT count(*) INTO hits FROM table_a a INNER JOIN table_b b ON (a.id=b.id) WHERE a.emp_checked<>b.mgr_checked;
IF hits>0 THEN
UPDATE table_a a SET emp_checked=(SELECT mgr_checked FROM table_b b WHERE a.id=b.id);
END IF;
END;
/
The key is don't use FOR EACH ROW, which will cause table lock.
Edit
This will NOT conflicts with table lock or ORA-04091, because there's no FOR EACH ROW in trigger condition (a.k.a. not a row trigger), which means the trigger body will only be executed AFTER the whole update action has been finished;
This will NOT cause an infinite loop, because when count(*) in trigger body returns 0, the UPDATE inside trigger body will not be executed;
Also, count(*) has to be used because this is a statement trigger and there's no :new.emp_checked available;
General scenario: Update table_a -> triggers tri_table_a -> [trigger]check count -> [trigger]update table_b ->triggers tri_table_b -> [trigger]check count -> done.
I tried this out myself before posting this answer.
I've created an update procedure that will change the values of a record in a table if the matching record in another table changed, however, how i've got it set up is that even though there is like 12 fields that can be updated, the procedure will update every one of them even if only 1 field has been changed
this is ofcause inefficient, it takes about 50 seconds for just 1 record, not good if there is like a few hundred, i thought up the psudocode for this easy enough
if olddata != newdata
UPDATE!!!
endif
thing is that i have little to no experience with if statements in SQL, if i could work with PHP then it would be easy by just running a SELECT with a WHERE clause much like the if statement above then checking the number of rows it returned (if 0 then there is no change) but the procedure has to be just SQL, the best i could come up with is this
IF(SELECT o.field FROM originaltbale AS o INNER JOIN updatetable AS u ON o.primarykey = u.primarykey WHERE o.field <> u.field) != 0
UPDATE!!!
ENDIF
but i doubt that'll work cause i'm sure SELECT won't return the number of rows, how can i create an if statement that'll work like this
I'm not sure I entirely understand what you're updating, but you're much better off doing this in a single statement and incorporating your IF into your WHERE clause.
UPDATE updatetable u
SET ... something ...
WHERE EXISTS ( SELECT 1 FROM originaltable o WHERE o.primarykey = u.primarykey AND ... )
But if you're storing the same field in two tables, are you sure this is the best database design for your tables?
If you want to know whether a row exists satisfying your criteria, then the EXISTS clause can be used.
-- If the original table doesn't have a record with the state same as that in updatetable
IF NOT EXISTS (SELECT o.field FROM originaltbale AS o INNER JOIN updatetable AS u ON o.primarykey = u.primarykey WHERE o.field <> u.field)
BEGIN
UPDATE !!!
END
Hope this helps.
I've got the following trigger;
CREATE TRIGGER trFLightAndDestination
ON checkin_flight
AFTER INSERT,UPDATE
AS
BEGIN
IF NOT EXISTS
(
SELECT 1
FROM Flight v
INNER JOIN Inserted AS i ON i.flightnumber = v.flightnumber
INNER JOIN checkin_destination AS ib ON ib.airport = v.airport
INNER JOIN checkin_company AS im ON im.company = v.company
WHERE i.desk = ib.desk AND i.desk = im.desk
)
BEGIN
RAISERROR('This combination of of flight and check-in desk is not possible',16,1)
ROLLBACK TRAN
END
END
What i want the trigger to do is to check the tables Flight, checkin_destination and checkin_company when a new record for checkin_flight is added. Every record of checkin_flight contains a flightnumber and desknumber where passengers need to check in for this destination.
The tables checkin_destination and checkin_company contain information about companies and destinations restricted to certain checkin desks. When adding a record to checkin_flight i need information from the flight table to get the destination and flightcompany with the inserted flightnumber. This information needs to be checked against the available checkin combinations for flights, destinations and companies.
I'm using the trigger as stated above, but when i try to insert a wrong combination the trigger allows it. What am i missing here?
EDIT 1:
I'm using the following multiple insert statement
INSERT INTO checkin_flight VALUES (5315,3),(5316,3),(5316,2)
//5315 is the flightnumber, 3 is the desknumber to checkin for that flight
EDIT 2:
Tested a single row insert which isn't possible, then the error is being thrown correct. So it's the multiple insert which seems to give the problem.
The problem is that your logic is allowing any insert that includes at least one valid set of values through. It will only fail if all of the inserted records are invalid, instead of if any of the inserted records are invalid.
Change your "IF NOT EXISTS(...)" to a statement "IF EXISTS(...)" and change your SELECT statement to return invalid flights.
eg:
IF EXISTS
(
SELECT 1
FROM Flight v
INNER JOIN Inserted AS i ON i.flightnumber = v.flightnumber
LEFT JOIN checkin_destination AS ib ON ib.airport = v.airport
AND i.desk = ib.desk
LEFT JOIN checkin_company AS im ON im.company = v.company
AND i.desk = im.desk
WHERE (im.desk IS NULL OR ib.desk IS NULL)
)
BEGIN
RAISERROR('This combination of of flight and check-in desk is not possible',16,1)
ROLLBACK TRAN
END
I'm not sure of your business logic, but you need to check that the query does the proper thing.
Your problem is the IF NOT EXISTS, if the condition is true for 1 of the 3 rows in INSERTED it does not exist. You need to convert it to find a problems row and use IF EXISTS then error out.
However, when in a trigger the best way to error out is:
RAISERROR()
ROLLBACK TRANSACTION
RETURN
I kind of doubt that the lack of a RETURN is your problem, but it is always best to include the three Rs when erroring out in a trigger.
The problem is that the condition will be true if only one of the inserted records are correct. You have to check that all records are correct, e.g.:
if (
(
select count(*) from inserted
) = (
select count(*) from flight v
inner join inserted i ...
)
) ...
The inserted table can contain multiple rows and therefore all logic within a trigger MUST be able to apply to all rows. The idea triggers must fire once per row effect is a common misunderstanding WRT triggers. SQL Server will tend to coalesce calls to a trigger to increase performance when they occur within the same transaction.
To fix you might start with a COUNT() of inserted and compare that with a COUNT() of the matching conditions and raise an error if there is a mismatch.