Error update trigger after new row has inserted into same table - sql-server

I want to update OrigOrderNbr and OrigOrderType (QT type) because when I create first both of column are Null value. But after S2 was created (QT converted to S2) the OrigOrderType and OrigOrderNbr (S2) take from QT reference. Instead of that, I want to update it to QT also.
http://i.stack.imgur.com/6ipFa.png
http://i.stack.imgur.com/E6qzT.png
CREATE TRIGGER tgg_SOOrder
ON dbo.SOOrder
FOR INSERT
AS
DECLARE #tOrigOrderType char(2),
#tOrigOrderNbr nvarchar(15)
SELECT #tOrigOrderType = i.OrderType,
#tOrigOrderNbr = i.OrderNbr
FROM inserted i
UPDATE dbo.SOOrder
SET OrigOrderType = #tOrigOrderType,
OrigOrderNbr = #tOrigOrderNbr
FROM inserted i
WHERE dbo.SOOrder.CompanyID='2'
and dbo.SOOrder.OrderType=i.OrigOrderType
and dbo.SOOrder.OrderNbr=i.OrigOrderNbr
GO
After I run that trigger, it showed the message 'Error #91: Another process has updated 'SOOrder' record. Your changes will be lost.'.

Per long string of comments, including some excellent suggestions in regards to proper trigger writing techniques by #marc_s and #Damien_The_Unbeliever, as well as my better understanding of your issue at this point, here's the re-worked trigger:
CREATE TRIGGER tgg_SOOrder
ON dbo.SOOrder
FOR INSERT
AS
--Update QT record with S2 record's order info
UPDATE SOOrder
SET OrigOrderType = 'S2'
, OrigOrderNbr = i.OrderNbr
FROM SOOrder dest
JOIN inserted i
ON dest.OrderNbr = i.OrigOrderNbr
WHERE dest.OrderType = 'QT'
AND i.OrderType = 'S2'
AND dest.CompanyID = 2 --Business logic constraint
AND dest.OrigOrderNbr IS NULL
AND dest.OrigOrderType IS NULL
Basically, the idea is to update any record of type "QT" once a matching record of type "S2" is created. Matching here means that OrigOrderNbr of S2 record is the same as OrderNbr of QT record. I kept your business logic constraint in regards to CompanyID being set to 2. Additionally, we only care to modify QT records that have OrigOrderNbr and OrigOrderType set to NULL.
This trigger does not rely on a single-row insert; it will work regardless of the number of rows inserted - which is far less likely to break down the line.

Related

SQL Trigger Works in Play but not Production

I created an SQL trigger in my Play database and it worked great. When I moved it over to Production, it suddenly won't work. We want the trigger to kick off whenever someone edits one of two custom fields in our database. The company who created the software already set up a trigger that kicks of any time a change is made to the database object (it just didn't track the changes made to custom fields). If I let my new trigger create a new record, I wound up with two audit records, so I changed my trigger to update the audit record the software company's trigger created. Could anyone tell me what I have done wrong? Here is my trigger:
USE [TmsEPrd]
GO
/****** Object: Trigger [dbo].[tr_Biograph_Udef_Audit_tracking] Script Date: 11/23/2020 10:22:57 AM ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
ALTER TRIGGER [dbo].[tr_Biograph_Udef_Audit_tracking] ON [dbo].[BIOGRAPH_MASTER] FOR UPDATE AS
BEGIN
IF EXISTS (SELECT 1 FROM deleted d
JOIN inserted i ON d.ID_NUM = i.ID_NUM
JOIN (SELECT ID_NUM, binary_checksum(UDEF_10A_1, UDEF_2A_4) AS inserted_checksum
FROM inserted) a ON i.ID_NUM = a.ID_NUM
JOIN (SELECT ID_NUM, binary_checksum(UDEF_10A_1, UDEF_2A_4) AS deleted_checksum
FROM deleted) b ON d.ID_NUM = b.ID_NUM
WHERE a.inserted_checksum <> b.deleted_checksum)
BEGIN
Update BIOGRAPH_HISTORY
set archive_job_name = 'UDEF_Change',
udef_2a_4 = i.udef_2a_4,
udef_2a_4_CHG = i.udef_2a_4_chg,
udef_10a_1 = i.udef_10a_1,
udef_10a_1_chg = i.udef_10a_1_chg
from
(select i.ID_NUM, SYSDATETIME()as job_time_a,
i.UDEF_10A_1, case when i.UDEF_10A_1 = d.UDEF_10A_1 then 0 when i.UDEF_10A_1 is null and d.UDEF_10A_1 is null then 0 else 1 end as UDEF_10A_1_CHG,
i.UDEF_2A_4, case when i.UDEF_2A_4 = d.UDEF_2A_4 then 0 when i.UDEF_2A_4 is null and d.UDEF_2A_4 is null then 0 else 1 end as UDEF_2A_4_CHG,
d.USER_NAME,d.JOB_NAME,d.JOB_TIME
FROM deleted d JOIN inserted i ON d.ID_NUM = i.ID_NUM) i
join BIOGRAPH_HISTORY b on i.ID_NUM = b.ID_NUM
where DATEDIFF(Minute, i.job_time_a, b.ARCHIVE_JOB_TIM) = 0
and b.ARCHIVE_JOB_NAME not like 'UDEF_Change%'
END;
END;
Try specifying #order = 'LAST' for your trigger. It might be that your trigger is executing first and not finding a record to update. In your test system, the trigger execution order might be reversed.
The order that triggers are created might affect trigger execution order, but this is not something to rely upon. When you think about it, this can be a headache. A test system that looks just like production can behave differently.
This is similar to relying upon a "natural" record order of a clustered index and not using a ORDER BY clause. A different execution plan can use a different index or go parallel resulting in a different or no order.

Set values on a new table from a historical table, before adding new table back into historical table

I would like to create a historical view of alerts in an application. To do this, I am grabbing all events and timestamping them, then uploading them into a MS SQL table. I would also like to be able to exempt certain objects from the total count by flagging either the finding (to exclude the finding across all systems) or the object in the finding (to exclude the object from all findings).
The idea is, I will have all previous alerts in the main table, then I will set an 'exemptobject' or 'exemptfinding' bit column in the row. When I re-run the script weekly, I will upload the results directly into a temporary table and then I would like to compare either the object or the finding for each object in the temporary table to the main database's 'object' or 'finding' and set the respective 'exemptobject' or 'exemptfinding' bit. Once all the temporary table's objects have any exemption bits set, insert the temporary table into the main table and drop the temporary table to keep a historical record.
This will give me duplicate findings and objects, so I am having difficulty with the merge command:
BEGIN TRANSACTION
MERGE INTO [dbo].[temp_table]
USING [dbo].[historical]
ON [dbo].[temp_table].[object] = [dbo].[historical].[object] OR
[dbo].[temp_table].[finding] = [dbo].[historical].[finding]
WHEN MATCHED THEN
UPDATE
SET [exemptfinding] = [dbo].[historical].[exemptfinding]
,[exemptobject] = [dbo].[historical].[exemptobject]
,[exemptdate] = [dbo].[historical].[exemptdate]
,[comments] = [dbo].[historical].[comments];
COMMIT
This seems to do what I want, but I see that the results are going to grow exponentially and I think it won't be sustainable for long.
BEGIN TRANSACTION
UPDATE [dbo].[temp]
SET [dbo].[temp].[exemptfinding] = [historical].[exemptfinding]
,[dbo].[temp].[exemptobject] = [historical].[exemptobject]
,[dbo].[temp].[exemptdate] = [historical].[exemptdate]
,[dbo].[temp].[comments] = [historical].[comments]
FROM [dbo].[temp] temp
INNER JOIN [dbo].[historical] historical
ON (
[temp].[finding] = [sci].[finding] OR
[temp].[object] = [sci].[object] OR
) AND
(
[historical].[exemptfinding] = 1 OR
[historical].[exemptobject] = 1
)
COMMIT
I feel like I need to normalize the database, but I can't think of a way to separate things out and be able to:
See a count of each finding based on date the script was run
Be able to drill down into each day and see all the findings, objects and recommendations for each
Control the count shown for each finding by removing 'exempted' findings OR objects.
I feel like there's something obvious I'm missing or I'm thinking about this incorrectly. Any help would be greatly appreciated!
EDIT - The following seems to do what I want, but as soon as I add an additional WHERE condition to the final result, the query time goes from 7 seconds to 90 seconds, so I fear it will not scale.
BEGIN TRANSACTION
UPDATE [dbo].[temp]
SET [dbo].[temp].[exemptrecommendation] = [historical].[exemptrecommendation]
,[dbo].[temp].[exemptfinding] = [historical].[exemptfinding]
,[dbo].[temp].[exemptobject] = [historical].[exemptobject]
,[dbo].[temp].[exemptdate] = [historical].[exemptdate]
,[dbo].[temp].[comments] = [historical].[comments]
FROM (
SELECT *
FROM historical h
WHERE EXISTS (
SELECT id
,recommendation
FROM temp t
WHERE (
t.id = s.id OR
t.recommendation = s.recommendation
)
)
) historical
WHERE [dbo].[temp].[recommendation] = [historical].[recommendation] OR
[dbo].[temp].[id] = [historical].[id]
COMMIT

Missing Insert Record on the table with Trigger (Oracle DB)

The scenario is the following -
OrderTable with Columns "OrderId" and "OrderType"
OrderRelationTable with Columns "OrderId" and "CustId"
OrderProcessTable with Columns "OrderId", "OrderType", "CustId", and "ProcessFlag"
The flow goes like this-
Application1 creates the record in OrderTable -> Then pass the record to Application2 by using MQ protocol, Application 2 in this case insert/create the record passed in the OrderRelationTable -> Then a trigger is called in Oracle DB to create the record in OrderProcessTable
Problem
Sometimes the record is not inserted into table 3 OrderProcessTable. Not sure if it is cause by timing or there is something that is not correct with the trigger?
Application1 Code
boolean updated = false;
/** JDBC prepare statement execution insert into OrderTable in Java**/
int rowCount = ps.executeUpdate();
if(rowCount>0){
updated=true;
}
log.log("updated flag:"+updated);
/** I am able to see the log shows the flag is true, and recored inserted into OrderTable **/
Application2 Code
This doesn't really matter much assuming that it is some Java JDBC code that does the insert into OrderRelationTable and it is successful.
The Trigger
Assuming the syntax is correct.
CREATE OR REPLACE TRIGGER INSERTINTOOrderProcessTable
AFTER INSERT ON OrderRelationTable
FOR EACH ROW DECLEAR
v_order_type := null;
BEGIN
SELECT OrderType INTO v_order_type FROM OrderTable
WHERE OrderId = :new.OrderId
AND OrderType IS NOT NULL
AND rownum=1;
IF v_order_type IS NOT NULL THEN
INSERT INTO OrderProcessTable VALUES (:new.OrderId, v_order_type, :new.CustId, 'N');
END IF;
END;
Questions -
After the Application 1 Code is executed is guaranteed DB will have the OrderTable record avaliable for SELECT statement? (Assume that updated flag is true)
Is there a timing issue with the app code and trigger? for example when trigger calls the SELECT statement from OrderTable? (of course the order id is matching in the OrderRelationTable and OrderTable)
Basically right now my problem is that sometimes (rarely) the record is not inserted into OrderProcessTable via the trigger even though it should (Order Type is not null)? Any idea?
There's no timing issue, as far as I can tell.
As of trigger code: what is the purpose of and rownum = 1 condition? I'm not saying that it is wrong, I'm just asking. Do you expect several rows to be returned by that query? If so, is that a legal situation? Wouldn't you rather handle it with the WHEN TOO_MANY_ROWS exception handler (i.e. instead of using the ROWNUM condition)?
What happens if SELECT returns nothing? It raises then NO_DATA_FOUND exception and trigger fails and certainly doesn't insert anything. Is it propagated so that someone (human being) or something (error logging procedure) sees / catches it so that you'd know that something went wrong?
And, of course, the fact that V_ORDER_TYPE remains NULL which causes INSERT to fail (as P. Salmon already suggested).

checking if the same row exists in the table or not [duplicate]

I've got a table with data named energydata
it has just three columns
(webmeterID, DateTime, kWh)
I have a new set of updated data in a table temp_energydata.
The DateTime and the webmeterID stay the same. But the kWh values need updating from temp_energydata table.
How do I write the T-SQL for this the correct way?
Assuming you want an actual SQL Server MERGE statement:
MERGE INTO dbo.energydata WITH (HOLDLOCK) AS target
USING dbo.temp_energydata AS source
ON target.webmeterID = source.webmeterID
AND target.DateTime = source.DateTime
WHEN MATCHED THEN
UPDATE SET target.kWh = source.kWh
WHEN NOT MATCHED BY TARGET THEN
INSERT (webmeterID, DateTime, kWh)
VALUES (source.webmeterID, source.DateTime, source.kWh);
If you also want to delete records in the target that aren't in the source:
MERGE INTO dbo.energydata WITH (HOLDLOCK) AS target
USING dbo.temp_energydata AS source
ON target.webmeterID = source.webmeterID
AND target.DateTime = source.DateTime
WHEN MATCHED THEN
UPDATE SET target.kWh = source.kWh
WHEN NOT MATCHED BY TARGET THEN
INSERT (webmeterID, DateTime, kWh)
VALUES (source.webmeterID, source.DateTime, source.kWh)
WHEN NOT MATCHED BY SOURCE THEN
DELETE;
Because this has become a bit more popular, I feel like I should expand this answer a bit with some caveats to be aware of.
First, there are several blogs which report concurrency issues with the MERGE statement in older versions of SQL Server. I do not know if this issue has ever been addressed in later editions. Either way, this can largely be worked around by specifying the HOLDLOCK or SERIALIZABLE lock hint:
MERGE INTO dbo.energydata WITH (HOLDLOCK) AS target
[...]
You can also accomplish the same thing with more restrictive transaction isolation levels.
There are several other known issues with MERGE. (Note that since Microsoft nuked Connect and didn't link issues in the old system to issues in the new system, these older issues are hard to track down. Thanks, Microsoft!) From what I can tell, most of them are not common problems or can be worked around with the same locking hints as above, but I haven't tested them.
As it is, even though I've never had any problems with the MERGE statement myself, I always use the WITH (HOLDLOCK) hint now, and I prefer to use the statement only in the most straightforward of cases.
I often used Bacon Bits great answer as I just can not memorize the syntax.
But I usually add a CTE as an addition to make the DELETE part more useful because very often you will want to apply the merge only to a part of the target table.
WITH target as (
SELECT * FROM dbo.energydate WHERE DateTime > GETDATE()
)
MERGE INTO target WITH (HOLDLOCK)
USING dbo.temp_energydata AS source
ON target.webmeterID = source.webmeterID
AND target.DateTime = source.DateTime
WHEN MATCHED THEN
UPDATE SET target.kWh = source.kWh
WHEN NOT MATCHED BY TARGET THEN
INSERT (webmeterID, DateTime, kWh)
VALUES (source.webmeterID, source.DateTime, source.kWh)
WHEN NOT MATCHED BY SOURCE THEN
DELETE
If you need just update your records in energydata based on data in temp_energydata, assuming that temp_enerydata doesn't contain any new records, then try this:
UPDATE e SET e.kWh = t.kWh
FROM energydata e INNER JOIN
temp_energydata t ON e.webmeterID = t.webmeterID AND
e.DateTime = t.DateTime
Here is working sqlfiddle
But if temp_energydata contains new records and you need to insert it to energydata preferably with one statement then you should definitely go with the answer that Bacon Bits gave.
UPDATE ed
SET ed.kWh = ted.kWh
FROM energydata ed
INNER JOIN temp_energydata ted ON ted.webmeterID = ed.webmeterID
Update energydata set energydata.kWh = temp.kWh
where energydata.webmeterID = (select webmeterID from temp_energydata as temp)
THE CORRECT WAY IS :
UPDATE test1
INNER JOIN test2 ON (test1.id = test2.id)
SET test1.data = test2.data

Optionally saving selectively data in one-go only when you want it

I have a table where data does not initially exist until an action is taken that stores all settings made by client in one-go. To illustrate this simply, a button click that stores all column values off a (HTML) table into a database table (let's call this dbo.Settings).
So instead of inserting into this dbo.Settings all the default values prior to user making any changes to their individual settings (ever), I kind of created the pseudo data for them that will be returned whenever requested, kind of like SELECT-ing the default values:
SELECT
CanView = ISNULL(CanView, 1),
CanRead = ISNULL(CanRead, 1),
CanWrite = ISNULL(CanWrite, 0)
FROM
dbo.Settings AS s
WHERE
UserId = #id
Rather than doing:
IF NOT EXISTS(SELECT * FROM dbo.Settings WHERE UserId = #id)
BEGIN
INSERT INTO dbo.Settings (UserId, CanView, CanRead, CanWrite)
VALUES (#id, 1, 1, 0)
END
The problem with this is whenever I need to add a new setting column in the future, I now have to note one more procedure to modify/add the default value for this column as well -- which I don't like. Using TRIGGER would be an option but I wonder what the best practice in managing data like this would be. Or would you do something like this:
CREATE PROC Settings_CreateOrModify
#userId INT,
#canView BIT = NULL,
#canRead BIT = NULL,
#canWrite BIT = NULL
AS
BEGIN
IF EXISTS(SELECT * FROM dbo.Settings WHERE UserId = #userId) BEGIN
UPDATE s
SET
CanView = #canView,
CanRead = #canRead,
CanWrite = #canWrite
FROM
dbo.Settings AS s
WHERE
s.UserId = #userId AND
(#canView IS NULL OR #canView <> s.CanView) AND
(#canRead IS NULL OR #canRead <> s.CanRead) AND
(#canWrite IS NULL OR #canWrite <> s.CanWrite)
END
ELSE BEGIN
INSERT INTO
dbo.Settings(UserId, CanView, CanRead, CanWrite)
SELECT
#userId, #canView, #canRead, #canWrite
END
END
How would you handle data structure like this? Any recommendation or correction would be greatly appreciated. Thanks in advance!
Your SP is a good way to go, and doing it like this is commonly called an "UPSERT".
It also looks to me as if the block:
(#canView IS NULL OR #canView <> s.CanView) AND
(#canRead IS NULL OR #canRead <> s.CanRead) AND
(#canWrite IS NULL OR #canWrite <> s.CanWrite)
is problematic since it causes the UPDATE to run only if ALL parameters changed their value. I don't think that's what you wanted to say. Just SET the three values regardless of what's already there.
You still end up with three places to change when you add a new setting: The Table, the Upsert and the Defaults.
One very different approach is this:
Apply the defaults to the columns in the table definition.
Whenever you need the values for a new user, do: INSERT INTO dbo.Settings(UserId) The defaults will fill the rest of the columns.
Now you can retrieve the values for ALL users (new or not) in the same way from the table.
Since you already inserted the user in step 2, you know the userid is there already and you can always save the changes via a simple update.
This eliminates the SP and the need of providing the defaults in one extra place.

Resources