2

I have a situation where the following DDL Statement is rising to the top of my list of processes which I have ordered by the number of processes that each process is blocking (directly or indirectly). Killing this process frees up everything. Before you kill it almost everything backs up behind it (almost, but not quite).

CREATE TRIGGER [dbo].[MailReturnCBRHandling] ON dbo.ACC 
AFTER UPDATE
AS

Declare 
@v_Agency_ID            varchar(15),
@n_SaleID           numeric,
@n_DebtorID         numeric,
@n_AccNo            numeric,
@v_AccStatID            varchar(20),
@b_AccStatMailReturned  bit,
@b_AccStatReportsToCBR  bit,
@b_DebtorMailReturned       bit,
@b_AccReportsToCBR      bit

IF UPDATE(Stat_ID)

BEGIN

DECLARE c_InsertedMailReturnCBR CURSOR FOR
    SELECT Agency_ID, SaleID , Debtor_ID,  AccNo , Stat_ID,  IsNull(IsBureau,0)
    FROM INSERTED

OPEN c_InsertedMailReturnCBR
FETCH NEXT FROM c_InsertedMailReturnCBR INTO @v_Agency_ID, @n_SaleID, @n_DebtorID, @n_AccNo, @v_AccStatID, @b_AccReportsToCBR 

WHILE @@FETCH_STATUS = 0
BEGIN
    -- *************************************************    Get AccStat MailReturned & ReportsToCBR      *************************************************
    DECLARE c_AccStat CURSOR FOR
        SELECT MailReturned ,  ReportsToCBR FROM AccStat WHERE Agency_ID = @v_Agency_ID AND AccStat = @v_AccStatID
    
    OPEN c_AccStat
    FETCH NEXT FROM c_AccStat INTO @b_AccStatMailReturned, @b_AccStatReportsToCBR
    
    -- *************************************************    Get Debtor MailReturned    *************************************************
    DECLARE c_Debtor CURSOR FOR 
        SELECT IsNull(MailReturned,0) FROM Debtor WHERE Agency_ID = @v_Agency_ID AND Debtor_ID =  @n_DebtorID

    OPEN c_Debtor
    FETCH NEXT FROM c_Debtor INTO @b_DebtorMailReturned
            
    -- *************************************************    UPDATE MailReturned     *************************************************

    IF  @b_AccStatMailReturned IS NOT NULL
    BEGIN
        IF @b_AccStatMailReturned = 1 AND @b_DebtorMailReturned = 0 
        BEGIN
            UPDATE Debtor SET MailReturned = 1 WHERE Debtor_ID =  @n_DebtorID
            INSERT INTO DebtorNote (AGENCY_ID, DEBTOR_ID, NOTE, Priority, StampTime, Supress) 
            VALUES (@v_Agency_ID, @n_DebtorID, 'Auto Generated Note --> Debtor Mail Returned changed from: False To: True','C', GetDate() ,0)
        END
    
        IF @b_AccStatMailReturned = 0 AND @b_DebtorMailReturned = 1
        BEGIN
            UPDATE Debtor SET MailReturned = 0 WHERE Debtor_ID =  @n_DebtorID
            INSERT INTO DebtorNote (AGENCY_ID, DEBTOR_ID, NOTE, Priority, StampTime, Supress) 
            VALUES (@v_Agency_ID, @n_DebtorID, 'Auto Generated Note --> Debtor Mail Returned changed from: True To: False','C', GetDate() ,0)
        END
    END
    -- *************************************************    UPDATE ReportsToCBR   *************************************************

    IF @b_AccStatReportsToCBR IS NOT NULL 
    BEGIN
        IF @b_AccStatReportsToCBR = 1 AND @b_AccReportsToCBR = 0 
        BEGIN
            UPDATE ACC SET IsBureau = 1  WHERE AccNo =  @n_AccNo
            INSERT INTO DebtorNote (AGENCY_ID, DEBTOR_ID, NOTE, Priority, StampTime, Supress,AccNo,SaleID) 
            VALUES (@v_Agency_ID, @n_DebtorID, 'Auto Generated Note --> Reports To Bureau changed from: False To: True','C', GetDate() , 0, @n_AccNo, @n_SaleID)
        END
    
        IF @b_AccStatReportsToCBR = 0 AND @b_AccReportsToCBR = 1
        BEGIN
            UPDATE ACC SET IsBureau = 0  WHERE AccNo =  @n_AccNo
            INSERT INTO DebtorNote (AGENCY_ID, DEBTOR_ID, NOTE, Priority, StampTime, Supress,AccNo,SaleID) 
            VALUES (@v_Agency_ID, @n_DebtorID, 'Auto Generated Note --> Reports To Bureau changed from: True To: False','C', GetDate() , 0, @n_AccNo, @n_SaleID)
        END
    END
    FETCH NEXT FROM c_InsertedMailReturnCBR INTO @v_Agency_ID, @n_SaleID, @n_DebtorID, @n_AccNo, @v_AccStatID, @b_AccReportsToCBR 
    CLOSE c_Debtor
    DEALLOCATE c_Debtor 
    CLOSE c_AccStat
    DEALLOCATE c_AccStat
END
CLOSE c_InsertedMailReturnCBR
DEALLOCATE c_InsertedMailReturnCBR
END

The trigger is written by the company that provides us with the software that we use to run out business. There is no need to create it because it already exists. It should instantly fail because an object of that name already exists.

The vendor provided this software runs the entire application logged in as SA. I have begged my management to make them run all sql under the windows credential of the logged in user but they claim that they have no way to force the software vendor to to that so the SQL process list does not tell me anything about who submitted the create trigger and under what circumstances. We are running SQL Server 2016 SP3.

I am wondering:

  1. Why would this suspend? Why not just return an error tha t says an object of that name already exists?

  2. Given that it has suspended, why do all the other tasks back up behind it? Put another way, what resource is this process holding that the others need.

I can't believe that they are all waiting to create or alter triggers or that something that this process needs to change is in a page that everyone else would want. It would be as if this process was able to put a lock on the entire database, but why?

This happens about every two weeks. Exact same trigger. I have not yet determined a set of circumstances that trigger it. Thank you.

0

2 Answers 2

3

As mentioned by others, this is not the CREATE running, it's just the trace showing the full batch text. You can get the actual statement currently running using the following snippet (where st is dm_exec_sql_text and qs is dm_exec_query_stats or dm_exec_requests or similar).

SUBSTRING(st.text, (qs.statement_start_offset/2)+1,   
        ((CASE qs.statement_end_offset  
          WHEN -1 THEN DATALENGTH(st.text)  
         ELSE qs.statement_end_offset  
         END - qs.statement_start_offset)/2) + 1)

I'll be honest, your real issue here is most likely the trigger code itself, which uses not one, not two but three cursors. And it's all unnecessary. Burn this trigger with fire, nuke from orbit, it's a total shambles.

You can do this with just two joined UPDATEs, using OUTPUT to get the logging notes. It's also missing a recursion check, as one of the updates is on ACC itself (why that is is unclear).

CREATE TRIGGER [dbo].[MailReturnCBRHandling] ON dbo.ACC 
AFTER UPDATE
AS

SET NOCOUNT ON;

IF NOT UPDATE(Stat_ID)  -- column was present in the UPDATE
  OR TRIGGER_NESTLEVEL(@@PROCID, 'AFTER', 'DML') > 1   -- this trigger is already called
  OR NOT EXISTS (SELECT 1 FROM inserted)  -- no rows
    RETURN;


UPDATE d
SET MailReturned = ast.MailReturned
  OUTPUT i.Agency_ID, d.Debtor_ID, 'Auto Generated Note --> Debtor Mail Returned changed from: ' + IIF(inserted.MailReturned = 0, 'True To: False', 'False To: True'), 'C', GetDate(), 0
  INTO DebtorNote (AGENCY_ID, DEBTOR_ID, NOTE, Priority, StampTime, Supress)
FROM inserted i
JOIN AccStat ast ON ast.Agency_ID = i.Agency_ID AND ast.AccStat   = i.Stat_ID
JOIN Debtor d  ON d.Agency_ID = i.Agency_ID AND d.Debtor_ID = i.Debtor_ID
WHERE ISNULL(d.MailReturned, 0) <> ast.MailReturned;


UPDATE acc
SET IsBureau = ast.ReportsToCBR
  OUTPUT i.Agency_ID, d.Debtor_ID, 'Auto Generated Note --> Reports To Bureau changed from: ' + IIF(inserted.IsBureau = 0, 'True To: False', 'False To: True'), 'C', GetDate(), 0, i.AccNo, i.SaleID
  INTO DebtorNote (AGENCY_ID, DEBTOR_ID, NOTE, Priority, StampTime, Supress, AccNo, SaleID)
FROM inserted i
JOIN AccStat ast ON ast.Agency_ID = i.Agency_ID AND ast.AccStat = i.Stat_ID
JOIN ACC acc ON acc.AccNo = i.AccNo
WHERE ISNULL(i.IsBureau, 0) <> ast.ReportsToCBR;

This should substantially speed up your trigger, and therefore hopefully make it much less likely for you to get big blocking issues.

3
  • The trigger was written by the software vendor. I asked my management yesterday for permission to replace it. I do not use cursors when I write triggers. I am not allowed to change anything in production without permission. I agree with your recommended approach. I really appreciate your suggestion for getting to the actual text. I can't wait to add it to my who(is active) stored procedure. I will write again as soon as I try that.
    – Ted Cohen
    Commented Oct 30 at 23:57
  • You should not change other peoples' software. Commented Nov 1 at 17:01
  • I'm perfectly happy changing or improving other people's software if it's junk, just need to make sure they don't realise. Commented Nov 2 at 18:24
2

Why would this suspend? Why not just return an error tha t says an object of that name already exists?

It is not actually trying to run the CREATE TRIGGER statement. The text being returned is from a session that is actually firing the trigger, but it is presented in sys.dm_exec_sql_text as the whole "CREATE" statement.

See this answer for details on using the statement offset to fetch the actual portion of the trigger being executed that is causing the issue: https://dba.stackexchange.com/a/62856/136353

Given that it has suspended, why do all the other tasks back up behind it? Put another way, what resource is this process holding that the others need.

You need to get the lock information to see what locks are being held by the blocking session and what locks are requested by the blocked sessions to understand why this is causing the issue.

I like to use Adam Machanic's sp_WhoIsActive for this as a lot of the hard work has been done for you (helpful links: One, Two).

My guess is the trigger firing session is holding locks on one or more of the Debtor, DebtorNote, Acc or AccStat table(s) and your other sessions are requesting locks on those tables, hence the blocking.

5
  • I had no idea that it was executing the trigger instead of creating it which made no sense. This gives me an entire new perspective. I will look at what is going on with new eyes FWIW, I built my "who" stored procedure based on sp_WhoIsActive. I augmented it by chasing every process counting all of the process being blocked by all of the processes being blocked all the way to the end of every chain then I sort the processes in order of who is blocking the most processes. I will wait until it happens again and then examine the locks being held and the processes waiting on them. Tx!!
    – Ted Cohen
    Commented Oct 30 at 3:56
  • Maybe this should be a separate question, but given that sp_whoisactive is showing us the trigger that is blocking, could we (or Adam) modify spwhoisactive to show the DML (query) that triggered the trigger in addition to the trigger that is holding the lock at the head of the chain?
    – Ted Cohen
    Commented Oct 30 at 4:37
  • @TedCohen "I augmented it by chasing every process counting all of the process being blocked by all of the processes being blocked all the way to the end of every chain" - If you're using the ProcessId and BlockedProcessId to recursively get that list, be wary that this methodology may return incorrect results sometimes due to the timing of execution and when a ProcessId may be recycled and used for a new process.
    – J.D.
    Commented Oct 30 at 20:07
  • That is not something that I have explicitly considered. My logic, naively assumes that I can follow each blocked process to the point where the last process in the chain is not blocking anyone. My theory is that each process along the way is it self blocked and so it can not be reused until the "head of the snake" is killed and the logjam is freed up. I will study my code. In general, refreshing the query can add more blocked processes but the process ids that were suspended remain there till we either kill a process or go find a process that is waiting on user input and provide it.
    – Ted Cohen
    Commented Oct 31 at 0:12
  • Adam beat me to it. He already coded the logic that I described. A user turns it on using @find_block_leaders. I just found it when I went to study my code to see if it was possible for it to produce incorrect results due to a process id being reused. I found his. Thank you for your comment J.D. I will be using Adam's code going forward.
    – Ted Cohen
    Commented Oct 31 at 6:03

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.