MSSQL - IF within a TRIGGER - sql-server

we're just migrating from mariadb (galera) to MSSQL.
One of our applications has a very special behaviour - from time to time (I have not found a pattern, the vendor uses very fancy AI-related stuff which noone can debug :-/) it will block the monitor-user of our loadbalancers because of too many connects, so the loadbalancer is no longer able to get the health state, suspends all services on all servers and the whole service is going down.
So I wrote a trigger which enables this user after he will be disabled.
I've already thought about a constraint which prohibits this, but then the application goes nuts if it will disable the user.
Anyway - in mysql this works perfectly for us:
delimiter $$
CREATE TRIGGER f5mon_no_disable AFTER UPDATE ON dpa_web_user
FOR EACH ROW
BEGIN
IF NEW.user_id = '99999999' AND NEW.enabled = 0 THEN
UPDATE dpa_web_user SET enabled = 1 WHERE user_id = '9999999';
END IF;
END$$
delimiter ;
I tried this in T-SQL (if it's important, it is MSSQL 2016)
CREATE TRIGGER f5mon_no_disable ON [dbo].[dpa_web_user]
AFTER UPDATE
AS
BEGIN
IF ( inserted.[user_id] = '9999999' AND inserted.[enabled] = 0 )
BEGIN
UPDATE dpa_web_user SET enabled = 1 WHERE user_id = '9999999';
END
END
I think it's the if-statement which is totally wrong in more than one way - but I do not have an idea how the syntax is in t-sql.
Thanks in advance for your help.

You can use IF EXISTS but you can't reference column values in inserted without set-based access to inserted:
IF EXISTS (SELECT 1 FROM inserted WHERE [user_id] = '9999999' AND [enabled] = 0)
BEGIN
UPDATE dpa_web_user SET enabled = 1 WHERE user_id = '9999999';
END
You may want to add AND enabled <> 1 to prevent updating a row for no reason.
You can do this in a single statement though:
UPDATE u
SET enabled = 1
FROM dbo.dpa_web_user AS u
INNER JOIN inserted AS i
ON u.[user_id] = i.[user_id]
WHERE u.[user_id] = '9999999'
AND i.[user_id] = '9999999'
AND u.enabled <> 1
AND i.enabled = 0;

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.

How do I set the correct transaction level?

I am using Dapper on ADO.NET. So at present I am doing the following:
using (IDbConnection conn = new SqlConnection("MyConnectionString")))
{
conn.Open());
using (IDbTransaction transaction = conn.BeginTransaction())
{
// ...
However, there are various levels of transactions that can be set. I think this is the various settings.
My first question is how do I set the transaction level (where I am using Dapper)?
My second question is what is the correct level for each of the following cases? In each of these cases we have multiple instances of a web worker (Azure) service running that will be hitting the DB at the same time.
I need to run monthly charges on subscriptions. So in a transaction I need to read a record and if it's due for a charge create the invoice record and mark the record as processed. Any other read of that record for the same purpose needs to fail. But any other reads of that record that are just using it to verify that it is active need to succeed.
So what transaction do I use for the access that will be updating the processed column? And what transaction do I use for the other access that just needs to verify that the record is active?
In this case it's fine if a conflict causes the charge to not be run (we'll get it the next day). But it is critical that we not charge someone twice. And it is critical that the read to verify that the record is active succeed immediately while the other operation is in its transaction.
I need to update a record where I am setting just a couple of columns. One use case is I set a new password hash for a user record. It's fine if other access occurs during this except for deleting the record (I think that's the only problem use case). If another web service is also updating that's the user's problem for doing this in 2 places simultaneously.
But it's key that the record stay consistent. And this includes the use case of "set NumUses = NumUses + #ParamNum" so it needs to treat the read, calculation, write of the column value as an atomic action. And if I am setting 3 column values, they all get written together.
1) Assuming that Invoicing process is an SP with multiple statements your best bet is to create another "lock" table to store the fact that invoicing job is already running e.g.
CREATE TABLE InvoicingJob( JobStarted DATETIME, IsRunning BIT NOT NULL )
-- Table will only ever have one record
INSERT INTO InvoicingJob
SELECT NULL, 0
EXEC InvoicingProcess
ALTER PROCEDURE InvoicingProcess
AS
BEGIN
DECLARE #InvoicingJob TABLE( IsRunning BIT )
-- Try to aquire lock
UPDATE InvoicingJob WITH( TABLOCK )
SET JobStarted = GETDATE(), IsRunning = 1
OUTPUT INSERTED.IsRunning INTO #InvoicingJob( IsRunning )
WHERE IsRunning = 0
-- job has been running for more than a day i.e. likely crashed without releasing a lock
-- OR ( IsRunning = 1 AND JobStarted <= DATEADD( DAY, -1, GETDATE())
IF NOT EXISTS( SELECT * FROM #InvoicingJob )
BEGIN
PRINT 'Another Job is already running'
RETURN
END
ELSE
RAISERROR( 'Start Job', 0, 0 ) WITH NOWAIT
-- Do invoicing tasks
WAITFOR DELAY '00:01:00' -- to simulate execution time
-- Release lock
UPDATE InvoicingJob
SET IsRunning = 0
END
2) Read about how transactions work: https://learn.microsoft.com/en-us/sql/t-sql/language-elements/transactions-transact-sql?view=sql-server-2017
https://learn.microsoft.com/en-us/sql/t-sql/statements/set-transaction-isolation-level-transact-sql?view=sql-server-2017
You second question is quite broad.

How to loop and/or wait for delay

I have imported over 400 million records to a dummy dimension table. I need to take my existing fact table and join to the dummy dimension to perform an update on the fact table. To avoid filling up the transaction logs, somebody suggested I perform a loop to update these records instead of performing an update to hundreds of millions of records at once. I have research loops and researched using a Wait For and Delays, but I am not for sure the best approach on writing the logic out.
Here is the sample update I need to perform:
Update f
set f.value_key = r.value_key
FROM [dbo].[FACT_Table] f
INNER JOIN dbo.dummy_table r ON f.some_key = r.some_Key
and r.calendar_key = f.calendar_key
WHERE f.date_Key > 20130101
AND f.date_key < 20141201
AND f.diff_key = 17
If anybody has a suggestion on the best way to write I would really appreciate it.
To avoid filling up the transaction log you CAN set your recovery model to SIMPLE on your dev machines - that will prevent transaction log bloating when tran log backups aren't done.
ALTER DATABASE MyDB SET RECOVERY SIMPLE;
If you want to perform your update faster use hint ie., (tablock).
Please don't do what the previous person suggested unless you really understand what else will happen. The most important result is that you lose the ability to do a point in time recovery. If you do a full recovery every night and a transaction log recovery every hour (or every 15 minutes) switching to a simple recovery model breaks the chain and you can only recover to the last full recovery time.
If you do it you have to do it (switch to simple) switch back to full and do a full backup and then switch back to doing log backups on a schedule. When the previous person suggest is like driving without bumpers to save on car weight. Sounds great until you hit something.
-- Run this section of code one time to create a global queue containing only the columns needed to identify a unique row to process. Update the SELECT statement as necessary.
IF OBJECT_ID('Tempdb.dbo.##GlobalQueue') IS NOT NULL
DROP TABLE ##GlobalQueue
SELECT diff_key, date_key INTO ##GlobalQueue FROM [dbo].[FACT_Table] f
INNER JOIN dbo.dummy_table r ON f.some_key = r.some_Key
and r.calendar_key = f.calendar_key
WHERE f.date_Key > 20130101
AND f.date_key < 20141201
AND f.diff_key = 17
-- Copy/paste the SQL below to run from multiple sessions/clients if you want to process things faster than the single session. Start with 1 session and move up from there as needed to ramp up processing load.
WHILE 1 = 1
BEGIN
DELETE TOP ( 10000 ) --Feel free to update to a higher number if desired depending on how big of a 'bite' you want it to take each time.
##Queue WITH ( READPAST )
OUTPUT Deleted.*
INTO #RowsToProcess
IF ##ROWCOUNT > 0
BEGIN
Update f
set f.value_key = r.value_key
FROM [dbo].[FACT_Table] f
INNER JOIN dbo.dummy_table r ON f.some_key = r.some_Key
INNER JOIN #RowsToProcess RTP ON r.some_key = RTP.diff_key
and f.calendar_key = RTP.calendar_key
DELETE FROM #RowsToProcess
--WAITFOR DELAY '00:01' --(Commented out 1 minute delay as running multiple sessions to process the records eliminates the need for that). Here just for demonstration purposes as it may be useful in other situations.
END
ELSE
BREAK
END
use top and <>
in an update you must use top (#)
while 1 = 1
begin
update top (100) [test].[dbo].[Table_1]
set lname = 'bname'
where lname <> 'bname'
if ##ROWCOUNT = 0 break
end
while 1 = 1
begin
Update top (100000) f
set f.value_key = r.value_key
FROM [dbo].[FACT_Table] f
INNER JOIN dbo.dummy_table r
ON f.some_key = r.some_Key
and r.calendar_key = f.calendar_key
AND f.date_Key > 20130101
AND f.date_key < 20141201
AND f.diff_key = 17
AND f.value_key <> r.value_key
if ##ROWCOUNT = 0 break
end

Default action of INSTEAD OF UPDATE trigger on MsSQL DB

I've got problem with INSTEAD OF trigger in MsSQL. I got an app with some error, and as a quick workaround I don't want user to modify one exact row in DB.
I created following trigger: (on table)
create trigger UglyWorkaround ON Configuration instead of update
as
begin
if (select COUNT(1) from inserted where Key='Key01' and Value<>'2') > 0 begin
update Configuration set Value='2' where Key='Key01'
end else begin
-- DEFAULT ACTION (do update as intended)
end;
end;
But I've got problem with determining, how to set default action.
Update Configuration set Value=inserted.Value where Key=inserted.Key doesn't work for me. Is there any way how to do this with triggers? (I know that the solution is bad, but I got no other option, as I can't change code now.)
inserted is a table, so try joining:
update c set c.Value = i.Value
from Configuration c
inner join inserted i on c.Key = i.Key
You could also filter out Key01 at the same time, and it wouldn't matter if they tried to update the value for Key01 to something other than 2.
update c set c.Value = i.Value
from Configuration c
inner join inserted i on c.Key = i.Key
where i.Key <> 'Key01'
The INSTEAD OF part means that you're going to have to update the other columns as well as the value one.
There's no need to do this with an if .. else as the logic can be done at column level
create trigger NicerWorkaround ON Configuration instead of update
as
begin
update c set
c.Value = CASE WHEN i.Key='Key01' THEN '2' ELSE i.Value END,
c.OtherColumn1 = i.OtherColumn1,
c.OtherColumn2 = i.OtherColumn2
from Configuration c
inner join inserted i on c.Key = i.Key
END;
If you want to prevent the edit perform a rollback on the change, why are you setting Value='2' when you find a matching row?
You don't have to supply a default action, doing nothing in the trigger will let the update happen.

How to do Sql Server CE table update from another table

I have this sql:
UPDATE JOBMAKE SET WIP_STATUS='10sched1'
WHERE JBT_TYPE IN (SELECT JBT_TYPE FROM JOBVISIT WHERE JVST_ID = 21)
AND JOB_NUMBER IN (SELECT JOB_NUMBER FROM JOBVISIT WHERE JVST_ID = 21)
It works until I turn it into a parameterised query:
UPDATE JOBMAKE SET WIP_STATUS='10sched1'
WHERE JBT_TYPE IN (SELECT JBT_TYPE FROM JOBVISIT WHERE JVST_ID = #jvst_id)
AND JOB_NUMBER IN (SELECT JOB_NUMBER FROM JOBVISIT WHERE JVST_ID = #jvst_id)
Duplicated parameter names are not allowed. [ Parameter name = #jvst_id ]
I tried this (which i think would work in SQL SERVER 2005 - although I haven't tried it):
UPDATE JOBMAKE
SET WIP_STATUS='10sched1'
FROM JOBMAKE JM,JOBVISIT JV
WHERE JM.JOB_NUMBER = JV.JOB_NUMBER
AND JM.JBT_TYPE = JV.JBT_TYPE
AND JV.JVST_ID = 21
There was an error parsing the query. [ Token line number = 3,Token line offset = 1,Token in error = FROM ]
So, I can write dynamic sql instead of using parameters, or I can pass in 2 parameters with the same value, but does someone know how to do this a better way?
Colin
Your second attempt doesn't work because, based on the Books On-Line entry for UPDATE, SQL CE does't allow a FROM clause in an update statement.
I don't have SQL Compact Edition to test it on, but this might work:
UPDATE JOBMAKE
SET WIP_STATUS = '10sched1'
WHERE EXISTS (SELECT 1
FROM JOBVISIT AS JV
WHERE JV.JBT_TYPE = JOBMAKE.JBT_TYPE
AND JV.JOB_NUMBER = JOBMAKE.JOB_NUMBER
AND JV.JVST_ID = #jvst_id
)
It may be that you can alias JOBMAKE as JM to make the query slightly shorter.
EDIT
I'm not 100% sure of the limitations of SQL CE as they relate to the question raised in the comments (how to update a value in JOBMAKE using a value from JOBVISIT). Attempting to refer to the contents of the EXISTS clause in the outer query is unsupported in any SQL dialect I've come across, but there is another method you can try. This is untested but may work, since it looks like SQL CE supports correlated subqueries:
UPDATE JOBMAKE
SET WIP_STATUS = (SELECT JV.RES_CODE
FROM JOBVISIT AS JV
WHERE JV.JBT_TYPE = JOBMAKE.JBT_TYPE
AND JV.JOB_NUMBER = JOBMAKE.JOB_NUMBER
AND JV.JVST_ID = 20
)
There is a limitation, however. This query will fail if more than one row in JOBVISIT is retuned for each row in JOBMAKE.
If this doesn't work (or you cannot straightforwardly limit the inner query to a single row per outer row), it would be possible to carry out a row-by-row update using a cursor.

Resources