Обработка нескольких записей в триггере MS SQL - PullRequest
6 голосов
/ 19 марта 2009

Мне приходится использовать триггеры в MSSQL впервые, ну, в общем, триггеры. Прочитав и проверив это сам, я теперь понимаю, что триггер срабатывает для каждой команды, а не для каждой строки, вставленной, удаленной или обновленной.

Все это немного статистики для рекламной системы. Наша основная таблица статистики довольно большая и не содержит данных таким образом, который имеет смысл в большинстве случаев. Он содержит по одной строке для каждого клика, просмотренного и т. Д. Объявления. Как пользователь, он более склонен к просмотру этого, так как в день X количество кликов Y и количество просмотров Z и т. Д. До сих пор мы делали это исключительно на основе SQL-запроса, получая такого рода отчет из основной таблицы, но по мере роста таблицы увеличивается время выполнения этого запроса. Из-за этого мы решили использовать триггеры для обновления другой таблицы и, следовательно, сделать это немного проще на сервере SQL.

Теперь моя проблема - заставить это работать с несколькими записями. Я создал две хранимые процедуры: одну для обработки операции вставки, а другую для удаления. Мой триггер вставки (написанный для работы с одной записью) затем захватывает данные из таблицы Inserted и отправляет их в хранимую процедуру. Триггер удаления работает таким же образом, и (очевидно?) Триггер обновления действует так же, как удаление + вставка.

Теперь моя проблема в том, как лучше всего это сделать с несколькими записями. Я пытался использовать курсор, но, насколько я мог читать и видеть себя, это работает очень плохо. Я также рассмотрел написание некоторых «проверок» - например, при проверке, чтобы увидеть, есть ли в командах несколько записей, а затем идти с курсором, а в остальном просто избегать этого. В любом случае, вот мое решение с курсором, и мне интересно, есть ли способ сделать это лучше?

CREATE TRIGGER [dbo].[TR_STAT_INSERT]
   ON  [iqdev].[dbo].[Stat]
   AFTER INSERT
AS 
BEGIN
    SET NOCOUNT ON;

    DECLARE @Date DATE 
    DECLARE @CampaignId BIGINT
    DECLARE @CampaignName varchar(500)
    DECLARE @AdvertiserId BIGINT
    DECLARE @PublisherId BIGINT
    DECLARE @Unique BIT
    DECLARE @Approved BIT
    DECLARE @PublisherEarning money
    DECLARE @AdvertiserCost money
    DECLARE @Type smallint

    DECLARE InsertCursor CURSOR FOR SELECT Id FROM Inserted
    DECLARE @curId bigint

    OPEN InsertCursor

    FETCH NEXT FROM InsertCursor INTO @curId

    WHILE @@FETCH_STATUS = 0
    BEGIN

        SELECT @Date = [Date], @PublisherId = [PublisherCustomerId], @Approved = [Approved], @Unique = [Unique], @Type = [Type], @AdvertiserCost = AdvertiserCost, @PublisherEarning = PublisherEarning
        FROM Inserted
        WHERE Id = @curId

        SELECT @CampaignId = T1.CampaignId, @CampaignName = T2.Name, @AdvertiserId = T2.CustomerId
        FROM Advert AS T1
        INNER JOIN Campaign AS T2 on T1.CampaignId = T2.Id
        WHERE T1.Id = (SELECT AdvertId FROM Inserted WHERE Id = @curId)

        EXEC ProcStatInsertTrigger @Date, @CampaignId, @CampaignName, @AdvertiserId, @PublisherId, @Unique, @Approved, @PublisherEarning, @AdvertiserCost, @Type

        FETCH NEXT FROM InsertCursor INTO @curId
    END

    CLOSE InsertCursor
    DEALLOCATE InsertCursor
END

Хранимая процедура довольно большая и интенсивная, и я не думаю, что есть способ избежать циклического перебора записей таблицы Inserted тем или иным способом (хорошо, возможно, есть, но я бы хотел Я тоже могу читать код: p), поэтому я не буду утомлять вас этим (если вы не хотите думать иначе). Итак, есть ли лучший способ сделать это, и если да, то как?

РЕДАКТИРОВАТЬ: Ну после запроса, вот sproc

CREATE PROCEDURE ProcStatInsertTrigger 
    @Date DATE,
    @CampaignId BIGINT,
    @CampaignName varchar(500),
    @AdvertiserId BIGINT,
    @PublisherId BIGINT,
    @Unique BIT,
    @Approved BIT,
    @PublisherEarning money,
    @AdvertiserCost money,
    @Type smallint
AS
BEGIN
    -- SET NOCOUNT ON added to prevent extra result sets from
    -- interfering with SELECT statements.
    SET NOCOUNT ON;
IF @Approved = 1
        BEGIN
            DECLARE @test bit

            SELECT @test = 1 FROM CachedStats WHERE [Date] = @Date AND CampaignId = @CampaignId AND CustomerId = @PublisherId

            IF @test IS NULL
                BEGIN
                    INSERT INTO CachedStats ([Date], CustomerId, CampaignId, CampaignName) VALUES (@Date, @PublisherId, @CampaignId, @CampaignName)
                END

            SELECT @test = NULL

                    DECLARE @Clicks int
                    DECLARE @TotalAdvertiserCost money
                    DECLARE @TotalPublisherEarning money
                    DECLARE @PublisherCPC money
                    DECLARE @AdvertiserCPC money

                    SELECT @Clicks = Clicks, @TotalAdvertiserCost = AdvertiserCost + @AdvertiserCost, @TotalPublisherEarning = PublisherEarning + @PublisherEarning FROM CachedStats
                    WHERE [Date] = @Date AND CustomerId = @PublisherId AND CampaignId = @CampaignId

                    IF @Type = 0 -- If click add one to the calculation
                        BEGIN
                            SELECT @Clicks = @Clicks + 1
                        END

                    IF @Clicks > 0
                        BEGIN
                            SELECT @PublisherCPC = @TotalPublisherEarning / @Clicks, @AdvertiserCPC = @TotalAdvertiserCost / @Clicks
                        END
                    ELSE
                        BEGIN
                            SELECT @PublisherCPC = 0, @AdvertiserCPC = 0
                        END
            IF @Type = 0
                BEGIN

                    UPDATE CachedStats SET
                        Clicks = @Clicks,
                        UniqueClicks = UniqueClicks + @Unique,
                        PublisherEarning = @TotalPublisherEarning,
                        AdvertiserCost = @TotalAdvertiserCost,
                        PublisherCPC = @PublisherCPC,
                        AdvertiserCPC = @AdvertiserCPC
                    WHERE [Date] = @Date AND CustomerId = @PublisherId AND CampaignId = @CampaignId
                END
            ELSE IF @Type = 1 OR  @Type = 4 -- lead or coreg
                BEGIN
                    UPDATE CachedStats SET
                        Leads = Leads + 1,
                        PublisherEarning = @TotalPublisherEarning,
                        AdvertiserCost = @TotalAdvertiserCost,
                        AdvertiserCPC = @AdvertiserCPC,
                        PublisherCPC = @AdvertiserCPC
                    WHERE [Date] = @Date AND CustomerId = @PublisherId AND CampaignId = @CampaignId
                END
            ELSE IF @Type = 3 -- Isale
                BEGIN
                    UPDATE CachedStats SET
                        Leads = Leads + 1,
                        PublisherEarning = @TotalPublisherEarning,
                        AdvertiserCost = @TotalAdvertiserCost,
                        AdvertiserCPC = @AdvertiserCPC,
                        PublisherCPC = @AdvertiserCPC,
                        AdvertiserOrderValue = @AdvertiserCost,
                        PublisherOrderValue = @PublisherEarning 
                    WHERE [Date] = @Date AND CustomerId = @PublisherId AND CampaignId = @CampaignId                 
                END
           ELSE IF @Type = 2 -- View
                BEGIN
                    UPDATE CachedStats SET
                        [Views] = [Views] + 1,
                        UniqueViews = UniqueViews + @Unique,
                        PublisherEarning = @TotalPublisherEarning,
                        AdvertiserCost = @TotalAdvertiserCost,
                        PublisherCPC = @PublisherCPC,
                        AdvertiserCPC = @AdvertiserCPC
                    WHERE [Date] = @Date AND CustomerId = @PublisherId AND CampaignId = @CampaignId         
                END
        END
END

После помощи, вот мой окончательный результат, опубликованный на тот случай, если у других есть похожая проблема

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 AS   a ON a.Id = i.AdvertId
        INNER JOIN Campaign AS c ON c.Id = a.CampaignId
    WHERE
        i.[Approved] = 1
        AND NOT EXISTS (
                SELECT 1 
                FROM CachedStats as t
                WHERE 
                        [Date] = CONVERT(Date, i.[Date])
                        AND CampaignId = c.Id 
                        AND CustomerId = i.[PublisherCustomerId]
                        AND t.AdvertId = i.AdvertId
        )

  -- update all affected records at once
    UPDATE 
        CachedStats
    SET
        Clicks = 
            Clicks + (
                SELECT COUNT(*) FROM Inserted s
                WHERE s.Approved = 1
                AND   s.PublisherCustomerId = i.PublisherCustomerId
                AND   CONVERT(Date, s.[Date]) = CONVERT(Date, i.[Date])
                AND   s.AdvertId = i.AdvertId
                AND   s.[Type] = 0
            ),
        UniqueClicks = 
            UniqueClicks + (
                SELECT COUNT(*) FROM Inserted s
                WHERE s.Approved = 1
                AND   s.[Unique] = 1
                AND   s.PublisherCustomerId = i.PublisherCustomerId
                AND   CONVERT(Date, s.[Date]) = CONVERT(Date, i.[Date])
                AND   s.AdvertId = i.AdvertId
                AND   s.[Type] = 0
            ),
        [Views] = 
            [Views] + (
                SELECT COUNT(*) FROM Inserted s
                WHERE s.Approved = 1
                AND   s.PublisherCustomerId = i.PublisherCustomerId
                AND   CONVERT(Date, s.[Date]) = CONVERT(Date, i.[Date])
                AND   s.AdvertId = i.AdvertId
                AND   s.[Type] = 2
            ),
        UniqueViews = 
            UniqueViews + (
                SELECT COUNT(*) FROM Inserted s
                WHERE s.Approved = 1
                AND   s.[Unique] = 1
                AND   s.PublisherCustomerId = i.PublisherCustomerId
                AND   CONVERT(Date, s.[Date]) = CONVERT(Date, i.[Date])
                AND   s.AdvertId = i.AdvertId
                AND   s.[Type] = 2
            ),
        Leads = 
            Leads + (
                SELECT COUNT(*) FROM Inserted s
                WHERE s.Approved = 1
                AND   s.[Unique] = 1
                AND   s.PublisherCustomerId = i.PublisherCustomerId
                AND   CONVERT(Date, s.[Date]) = CONVERT(Date, i.[Date])
                AND   s.AdvertId = i.AdvertId
                AND   s.[Type] IN (1,3,4)
            ),
        PublisherEarning =
            CachedStats.PublisherEarning + ISNULL((
                SELECT SUM(PublisherEarning) FROM Inserted s
                WHERE s.Approved = 1
                AND   s.PublisherCustomerId = i.PublisherCustomerId
                AND   CONVERT(Date, s.[Date]) = CONVERT(Date, i.[Date])
                AND   s.AdvertId = i.AdvertId           

            ), 0),
        AdvertiserCost =
            CachedStats.AdvertiserCost + ISNULL((
                SELECT SUM(AdvertiserCost) FROM Inserted s
                WHERE s.Approved = 1
                AND   s.PublisherCustomerId = i.PublisherCustomerId
                AND   CONVERT(Date, s.[Date]) = CONVERT(Date, i.[Date])
                AND   s.AdvertId = i.AdvertId
            ), 0),
        PublisherOrderValue =
            PublisherOrderValue + ISNULL((
                SELECT SUM(PublisherEarning) FROM Inserted s
                WHERE s.Approved = 1
                AND   s.PublisherCustomerId = i.PublisherCustomerId
                AND   CONVERT(Date, s.[Date]) = CONVERT(Date, i.[Date])
                AND   s.AdvertId = i.AdvertId
                AND   s.[Type] = 3              
            ), 0),
        AdvertiserOrderValue =
            AdvertiserOrderValue + ISNULL((
                SELECT SUM(AdvertiserCost) FROM Inserted s
                WHERE s.Approved = 1
                AND   s.PublisherCustomerId = i.PublisherCustomerId
                AND   CONVERT(Date, s.[Date]) = CONVERT(Date, i.[Date])
                AND   s.AdvertId = i.AdvertId           
                AND   s.[Type] = 3
            ), 0),
        PublisherCPC = 
            CASE WHEN (Clicks + (
                SELECT COUNT(*) FROM Inserted s
                WHERE s.Approved = 1
                AND   s.PublisherCustomerId = i.PublisherCustomerId
                AND   CONVERT(Date, s.[Date]) = CONVERT(Date, i.[Date])
                AND   s.AdvertId = i.AdvertId
                AND   s.[Type] = 0
            )) > 0 THEN
                (CachedStats.PublisherEarning + ISNULL((
                SELECT SUM(PublisherEarning) FROM Inserted s
                WHERE s.Approved = 1
                AND   s.PublisherCustomerId = i.PublisherCustomerId
                AND   CONVERT(Date, s.[Date]) = CONVERT(Date, i.[Date])
                AND   s.AdvertId = i.AdvertId           
            ), 0)) -- COST ^
                / (
                    Clicks + (
                        SELECT COUNT(*) FROM Inserted s
                        WHERE s.Approved = 1
                        AND   s.PublisherCustomerId = i.PublisherCustomerId
                        AND   CONVERT(Date, s.[Date]) = CONVERT(Date, i.[Date])
                        AND   s.AdvertId = i.AdvertId
                        AND   s.[Type] = 0
                    )               
                ) --- Clicks ^
            ELSE
                0
            END,    
        AdvertiserCPC = 
            CASE WHEN (Clicks + (
                SELECT COUNT(*) FROM Inserted s
                WHERE s.Approved = 1
                AND   s.PublisherCustomerId = i.PublisherCustomerId
                AND   CONVERT(Date, s.[Date]) = CONVERT(Date, i.[Date])
                AND   s.AdvertId = i.AdvertId
                AND   s.[Type] = 0
            )) > 0 THEN
                (CachedStats.AdvertiserCost + ISNULL((
                SELECT SUM(AdvertiserCost) FROM Inserted s
                WHERE s.Approved = 1
                AND   s.PublisherCustomerId = i.PublisherCustomerId
                AND   CONVERT(Date, s.[Date]) = CONVERT(Date, i.[Date])
                AND   s.AdvertId = i.AdvertId           
            ), 0)) -- COST ^
                / (
                    Clicks + (
                        SELECT COUNT(*) FROM Inserted s
                        WHERE s.Approved = 1
                        AND   s.PublisherCustomerId = i.PublisherCustomerId
                        AND   CONVERT(Date, s.[Date]) = CONVERT(Date, i.[Date])
                        AND   s.AdvertId = i.AdvertId
                        AND   s.[Type] = 0
                    )               
                ) --- Clicks ^
            ELSE
                0
            END     
   FROM
        Inserted i
    WHERE
        i.Approved = 1 AND
        CachedStats.Advertid = i.AdvertId AND
        CachedStats.[Date] = Convert(Date, i.[Date]) AND
        CachedStats.CustomerId = i.PublisherCustomerId
  SET NOCOUNT OFF
END

Теперь это выглядит немного по-другому, потому что мне пришлось индексировать его также для каждого объявления - но большое спасибо за помощь - ускорило все с 30 часов до 30 секунд, чтобы сгенерировать CachedStats из моей собственной таблицы статистики разработки:)

Ответы [ 5 ]

8 голосов
/ 19 марта 2009

Хитрость в подобных ситуациях заключается в том, чтобы превратить последовательную операцию (для каждой записи xyz ) в операцию на основе набора (оператор UPDATE).

Я проанализировал вашу хранимую процедуру и объединил ваши отдельные операторы UPDATE в один. Этот единственный оператор затем может быть преобразован в версию, которая может быть применена ко всем вставленным записям одновременно, устраняя необходимость в хранимой процедуре и, следовательно, необходимость в курсоре.

РЕДАКТИРОВАТЬ: Ниже приведен код, который мы наконец-то начали работать. Время выполнения всей операции сократилось с «практически навсегда» (для оригинального решения) до чего-то меньше одной секунды, согласно отзывам ОП. Общий размер кода также заметно уменьшился.

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

Операции с таблицей CachedStats значительно выиграют от использования одного индекса из нескольких столбцов по сравнению с (Advertid, CustomerId, [Date]) (что подтверждается OP).

1 голос
/ 19 марта 2009

В зависимости от того, какую версию MSSQL вы используете, вам также следует рассмотреть возможность использования индексированных представлений для этого. Это вполне может быть более простой подход, чем ваши триггеры, в зависимости от того, как выглядит запрос отчета. См. здесь для получения дополнительной информации.

Кроме того, в вашем триггере вы должны попытаться записать свои обновления в материализованную таблицу результатов в виде операции на основе набора, а не курсора. Написание триггера на основе курсора потенциально может просто переместить вашу проблему из запроса отчета в вставки таблицы.

0 голосов
/ 19 марта 2009

Лучше всего перейти к триггерной операции на основе набора. Я не собираюсь писать это для вас на 100%, но позвольте мне начать, и мы увидим, откуда мы пойдем. Имейте в виду, я пишу это без таблиц / схем, и поэтому я не собираюсь проверять. Ожидайте опечаток: -)

Давайте сначала посмотрим на ваши операторы обновления. Из того, что я могу сказать, вы обновляете ту же таблицу с тем же предложением где, единственное отличие - столбцы. Вы можете объединить это, чтобы выглядеть так:

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      

Я согласен с вами, что это может быть ужасно, но это решение, которое вам придется принять.

Что касается вашего предложения вставки, я бы обработал это так же, как вы уже просто вставляете в таблицу из вставленной таблицы то, что еще не существует.

0 голосов
/ 19 марта 2009

Вы можете немного оптимизировать изменение курсора, используя опции FAST_FORWARD, READ_ONLY и LOCAL для курсора. Кроме того, вы вводите идентификатор в курсор, а затем возвращаетесь к нему, чтобы получить значения. Либо используйте CURRENT_OF, либо бросьте их все в переменные. Но я не ожидал бы, что эти перемены сильно вас купят.

Вам действительно нужно перейти к подходу, основанному на множестве. Этот сохраненный процесс определенно выполним в модели, основанной на множествах - хотя может потребоваться 3 или 4 различных оператора обновления. Но даже 3 или 4 разных триггера (1 для просмотров, 1 для кликов и т. Д.) Были бы лучше, чем курсорный подход.

0 голосов
/ 19 марта 2009

Первое, что я хотел бы сделать, это использовать курсор FAST_FORWARD вместо этого. Поскольку вы переходите только с одной записи на другую и не делаете никаких обновлений, это значительно улучшит производительность.

ОБЪЯВИТЬ о синтаксисе КУРСОРА

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...