Handling multiple records in a MS SQL trigger

前端 未结 5 477
猫巷女王i
猫巷女王i 2020-12-13 16:43

I am having to use triggers in MSSQL for the first time, well triggers in general. Having read around and tested this myself I realise now that a trigger fires per command a

相关标签:
5条回答
  • 2020-12-13 17:03

    Your best bet is to move to a set based operation in the trigger. I'm not going write this for you 100% but let me get you started, and we can see where we go from there. Keep in mind I am writting this without tables / schemas and so I'm not going validate. Expect Typos:-)

    Let's look at your update statements first, From what I can tell you are updating the same table with the same where clause the only difference is the columns. You can consolidate this to look like:

    UPDATE CachedStats SET
            /* Basically we are going to set the counts based on the type inline in the update clause*/
    
        Leads= CASE WHEN (@Type = 1 OR  @Type = 4 OR @Type=3 ) THEN Leads + 1 ELSE LEADS END,
            Clicks=CASE WHEN (@Type=0) THEN Clicks+1 ELSE Clicks END,
        Views=CASE WHEN (@Type=4) THEN Views+1 ELSE Views END,
            PublisherEarning = @PublisherEarning + PublisherEarning,
            AdvertiserCost = @AdvertiserCost +AdvertiserCost,
    FROM CachedStats CS
    INNER JOIN Inserted INS
        ON CS.Date=Inserted.Date AND CS.CustomerId=Ins.PublisherId AND CS.CampaignId=Ins.CampaignId      
    

    I do aggree with you that this could get ugly but that's a decision you'll have to make.

    As for your insert clause, I would handle that the same way you already are just insert into the table from the Inserted table whatever doesn't already exist.

    0 讨论(0)
  • 2020-12-13 17:05

    The trick with these kinds of situations is to turn the sequential operation (for each record do xyz) into a set-based operation (an UPDATE statement).

    I have analyzed your stored procedure and merged your separate UPDATE statements into a single one. This single statement can then be transformed into a version that can be applied to all inserted records at once, eliminating the need for a stored procedure and thereby the need for a cursor.

    EDIT: Below is the code that we finally got working. Execution time for the whole operation went down from "virtually forever" (for the original solution) to something under one second, according to the OP's feedback. Overall code size also decreased quite noticeably.

    CREATE TRIGGER [dbo].[TR_STAT_INSERT]
       ON  [iqdev].[dbo].[Stat]
       AFTER INSERT
    AS 
    BEGIN
      SET NOCOUNT ON
    
      -- insert all missing "CachedStats" rows
      INSERT INTO
        CachedStats ([Date], AdvertId, CustomerId, CampaignId, CampaignName) 
      SELECT DISTINCT
        CONVERT(Date, i.[Date]), i.AdvertId, i.PublisherCustomerId, c.Id, c.Name
      FROM
        Inserted i
        INNER JOIN Advert   a ON a.Id = i.AdvertId
        INNER JOIN Campaign c ON c.Id = a.CampaignId
      WHERE
        i.Approved = 1
        AND NOT EXISTS ( 
          SELECT 1 
          FROM   CachedStats
          WHERE  Advertid   = i.AdvertId AND
                 CustomerId = i.PublisherCustomerId AND
                 [Date]     = CONVERT(DATE, i.[Date])
        )
    
      -- update all affected records at once
      UPDATE 
        CachedStats
      SET
        Clicks               = Clicks               + i.AddedClicks,
        UniqueClicks         = UniqueClicks         + i.AddedUniqueClicks,
        [Views]              = [Views]              + i.AddedViews,
        UniqueViews          = UniqueViews          + i.AddedUniqueViews,
        Leads                = Leads                + i.AddedLeads,
        PublisherEarning     = PublisherEarning     + ISNULL(i.AddedPublisherEarning, 0),
        AdvertiserCost       = AdvertiserCost       + ISNULL(i.AddedAdvertiserCost, 0),
        PublisherOrderValue  = PublisherOrderValue  + ISNULL(i.AddedPublisherOrderValue, 0),
        AdvertiserOrderValue = AdvertiserOrderValue + ISNULL(i.AddedAdvertiserOrderValue, 0)
      FROM
        (
        SELECT
          AdvertId,
          CONVERT(DATE, [Date]) [Date],
          PublisherCustomerId,
          COUNT(*) NumRows,
          SUM(CASE WHEN Type IN (0)                      THEN 1 ELSE 0 END) AddedClicks,
          SUM(CASE WHEN Type IN (0)     AND [Unique] = 1 THEN 1 ELSE 0 END) AddedUniqueClicks,
          SUM(CASE WHEN Type IN (2)                      THEN 1 ELSE 0 END) AddedViews,
          SUM(CASE WHEN Type IN (2)     AND [Unique] = 1 THEN 1 ELSE 0 END) AddedUniqueViews,
          SUM(CASE WHEN Type IN (1,3,4) AND [Unique] = 1 THEN 1 ELSE 0 END) AddedLeads,
          SUM(PublisherEarning)                                      AddedPublisherEarning,
          SUM(AdvertiserCost)                                        AddedAdvertiserCost,
          SUM(CASE WHEN Type IN (3) THEN PublisherOrderValue  ELSE 0 END) AddedPublisherOrderValue,
          SUM(CASE WHEN Type IN (3) THEN AdvertiserOrderValue ELSE 0 END) AddedAdvertiserOrderValue
        FROM
          Inserted
        WHERE
          Approved = 1
        GROUP BY
          AdvertId,
          CONVERT(DATE, [Date]),
          PublisherCustomerId
        ) i 
        INNER JOIN CachedStats cs ON 
          cs.Advertid   = i.AdvertId AND
          cs.CustomerId = i.PublisherCustomerId AND
          cs.[Date]     = i.[Date]
    
      SET NOCOUNT OFF
    END
    

    The operations involving the CachedStats table will greatly benefit from one multiple-column index over (Advertid, CustomerId, [Date]) (as confirmed by the OP).

    0 讨论(0)
  • 2020-12-13 17:11

    First thing I would do is use a FAST_FORWARD cursor instead. As you are only going from one record to the next and not doing any updates this will be much better for performance.

    DECLARE CURSOR syntax

    0 讨论(0)
  • 2020-12-13 17:20

    Depending on what version of MSSQL you are running, you should also consider using Indexed Views for this as well. That could very well be a simpler approach than your triggers, depending on what the report query looks like. See here for more info.

    Also, in your trigger, you should try to write your updates to the materialized results table as a set based operation, not a cursor. Writing a cursor based trigger could potentially just be moving your problem from the report query to your table inserts instead.

    0 讨论(0)
  • 2020-12-13 17:20

    You can slightly optimize your cursor variation by doing FAST_FORWARD, READ_ONLY and LOCAL options on the cursor. Also, you're pulling the Id into your cursor, and then looping back to get the values. Either use CURRENT_OF or throw them all into variables. But, I wouldn't expect these changes to buy you much.

    You really need to move to a set based approach. That stored proc is definitely doable in a set based model - though it may take 3 or 4 different update statements. But even 3 or 4 different triggers (1 for views, 1 for clicks, etc.) would be better than the cursor approach.

    0 讨论(0)
提交回复
热议问题