Странная проблема с триггером, когда я делаю INSERT в таблицу

У меня есть триггер, прикрепленный к столу.

ALTER TRIGGER [dbo].[UpdateUniqueSubjectAfterInsertUpdate]
   ON  [dbo].[Contents]
   AFTER INSERT,UPDATE
AS
BEGIN

-- Grab the Id of the row just inserted/updated
DECLARE @Id INT

SELECT @Id = Id
FROM INSERTED

END

Каждый раз, когда вставляется или изменяется новая запись, я хочу обновить одно поле (в этой таблице). Ради этого вопроса представьте, что я обновляю поле LastModifiedOn (datetime).

Итак, у меня есть пакетная вставка.

INSERT INTO [dbo].[Contents]
SELECT Id, a, b, c, d, YouDontKnowMe
FROM [dbo].[CrapTable]

Теперь все строки вставлены правильно. Поле LastModifiedOn по умолчанию имеет значение null. Таким образом, все записи для этого нулевые -- ЗА ИСКЛЮЧЕНИЕМ первой строки.

Означает ли это, что триггер вызывается НЕ для каждой строки, которая вставляется в таблицу, а один раз ПОСЛЕ завершения запроса на вставку, т.е. ВСЕ строки вставлены? Что означает, что таблица INSERTED (в триггере) имеет не одну, а n строк?!

Если да... э-э... :( Значит ли это, что мне понадобится курсор в этом триггере? (если мне нужно сделать какую-то уникальную логику для каждой отдельной строки, что я делаю сейчас).

?

ОБНОВИТЬ

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

BEGIN
    SET NOCOUNT ON

    DECLARE @ContentId INTEGER,
        @ContentTypeId TINYINT,
        @UniqueSubject NVARCHAR(200),
        @NumberFound INTEGER
    
    -- Grab the Id. Also, convert the subject to a (first pass, untested)
    -- unique subject.
    -- NOTE: ToUriCleanText just replaces bad uri chars with a ''. 
    --   eg. an '#' -> ''
    SELECT @ContentId = ContentId, @ContentTypeId = ContentTypeId, 
        @UniqueSubject = [dbo].[ToUriCleanText]([Subject])
    FROM INSERTED
    
    -- Find out how many items we have, for these two keys.
    SELECT @NumberFound = COUNT(ContentId)
    FROM [dbo].[Contents]
    WHERE ContentId = @ContentId
        AND UniqueSubject = @UniqueSubject
    
    -- If we have at least one identical subject, then we need to make it 
    -- unique by appending the current found number.
    -- Eg. The first instance has no number. 
    --     Second instance has subject + '1',
    --     Third instance has subject + '2', etc...
    IF @NumberFound > 0
        SET @UniqueSubject = @UniqueSubject + CAST(@NumberFound AS NVARCHAR(10))

    -- Now save this change.
    UPDATE [dbo].[Contents]
    SET UniqueSubject = @UniqueSubject
    WHERE ContentId = @ContentId
END

person Pure.Krome    schedule 13.06.2009    source источник
comment
Обновленный ответ. Не уверен, что вы пытаетесь сделать с COUNT (ContentId), потому что предложение WHERE в любом случае ограничивает его до 0 или 1 строки   -  person gbn    schedule 13.06.2009
comment
какой-нибудь ответ здесь удовлетворил ваш вопрос?   -  person gbn    schedule 17.11.2011
comment
Приветствую - ответил. Все мои триггеры теперь обрабатывают несколько наборов записей... как надо :) И не с курсором...   -  person Pure.Krome    schedule 17.11.2011


Ответы (3)


Почему бы не изменить триггер для работы с несколькими строками? Курсор или циклы не нужны: в этом весь смысл SQL...

UPDATE
    dbo.SomeTable
SET
    LastModifiedOn = GETDATE()
WHERE
    EXIST (SELECT * FROM INSERTED I WHERE I.[ID] = dbo.SomeTable.[ID]

Редактировать: Что-то вроде...

INSERT @ATableVariable
    (ContentId, ContentTypeId, UniqueSubject)
SELECT 
    ContentId, ContentTypeId, [dbo].[ToUriCleanText]([Subject])
FROM
    INSERTED

UPDATE
    [dbo].[Contents]
SET
    UniqueSubject + CAST(NumberFound AS NVARCHAR(10))
FROM
    --Your original COUNT feels wrong and/or trivial
    --Do you expect 0, 1 or many rows.
    --Edit2: I assume 0 or 1 because of original WHERE so COUNT(*) will suffice
    -- .. although, this implies an EXISTS could be used but let's keep it closer to OP post
    (
    SELECT ContentId, UniqueSubject, COUNT(*) AS NumberFound
    FROM @ATableVariable
    GROUP BY ContentId, UniqueSubject
    HAVING COUNT(*) > 0
    ) foo
    JOIN
    [dbo].[Contents] C ON C.ContentId = foo.ContentId AND C.UniqueSubject = foo.UniqueSubject

Редактировать 2: и снова с РЕЙТИНГОМ

UPDATE
    C
SET
    UniqueSubject + CAST(foo.Ranking - 1 AS NVARCHAR(10))
FROM
    (
    SELECT
        ContentId, --not needed? UniqueSubject,
        ROW_NUMBER() OVER (PARTITION BY ContentId ORDER BY UniqueSubject) AS Ranking
    FROM
        @ATableVariable
    ) foo
JOIN
    dbo.Contents C ON C.ContentId = foo.ContentId 
    /* not needed? AND C.UniqueSubject = foo.UniqueSubject */
WHERE
foo.Ranking > 1
person gbn    schedule 13.06.2009
comment
+1 точно - таблица INSERTED содержит несколько строк - просто обновите все эти строки! :-) - person marc_s; 13.06.2009
comment
потому что я делаю некоторые специальные вещи для каждой строки. На самом деле я создаю уникальную текстовую строку (которая должна использоваться как часть URI) для каждого текстового поля «Тема». Поэтому для каждого предмета мне нужно проверить, существует ли он уже. если это так, сделайте его уникальным, добавив в конец число. Число — это количество (например, сколько раз существует исходный субъект). Так что это не так просто, как LastModifiedOn. Я просто хотел использовать это как простой пример. - person Pure.Krome; 13.06.2009
comment
Я обновил вступительный пост, чтобы включить в него свой специальный журнал. - person Pure.Krome; 13.06.2009
comment
Спасибо gdn за ответ. Я пока не уверен, что логика верна. Таблица INSERTED может содержать несколько строк. Теперь для каждой строки нам нужно установить уникальную тему для этой строки. Все идет нормально. НО, значение должно быть очищенной темой (например, ToUriCleanText) + число. Это число должно начинаться с 1, если эта строка является вторым или более высоким экземпляром. Я также думал об использовании ROWNUMBER (OVER ContentId). таким образом, я знаю, что номер ВСЕГДА будет одним и тем же, потому что ContentID является идентификатором PK. Я не думаю, что этот запрос делает все это ??? (я весьма озадачен). искренние извинения. - person Pure.Krome; 14.06.2009

Триггер будет запущен только один раз для запроса INSERT INTO. Таблица INSERTED будет содержать несколько строк.

person Andomar    schedule 13.06.2009
comment
стонать неееет. :( Как и в ... это не тот ответ, который я хотел услышать: P Ура, приятель, за быстрый ответ :) - person Pure.Krome; 13.06.2009
comment
Перейдите к решению без триггеров, если это вообще возможно; например, делайте вставки из хранимых процедур. Может избавить вас от многих головных болей :) - person Andomar; 13.06.2009
comment
вы можете просто использовать курсор для прокрутки Inserted и делать то, что вам нужно. Это не нарушение условий сделки. - person Jonathan Rupp; 13.06.2009
comment
@Jonathan: Продвигайте мышление, основанное на установках, особенно при работе с SQL! Курсоры, по большому счету, костыли, которые медленные. Обычно (не всегда) их следует избегать. - person Eric; 13.06.2009

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

.1. Создал индексированное представление, представляющее поле «Тема», которое необходимо очистить. Это поле должно быть уникальным... но прежде чем мы сможем сделать его уникальным, нам нужно сгруппировать по нему.

-- Create the view.
CREATE VIEW ContentsCleanSubjectView with SCHEMABINDING AS
SELECT ContentId, ContentTypeId, 
    [dbo].[ToUriCleanText]([Subject]) AS CleanedSubject
FROM [dbo].[Contents]
GO

-- Index the view with three index's. Custered PK and a non-clustered, 
-- which is where most of the joins will be done against.
-- Last one is because the execution plan reakons i was missing statistics
-- against one of the fields, so i added that index and the stats got gen'd.
CREATE UNIQUE CLUSTERED INDEX PK_ContentsCleanSubjectView ON 
    ContentsCleanSubjectView(ContentId)
CREATE NONCLUSTERED INDEX IX_BlahBlahSnipSnip_A ON 
    ContentsCleanSubjectView(ContentTypeId, CleanedSubject)
CREATE INDEX IX_BlahBlahSnipSnip_B ON
    ContentsCleanSubjectView(CleanedSubject)

.2. Создайте код триггера, который теперь
а) захватывает все элементы, "измененные" (ничего нового/сложного в этом)
б) упорядочивает все вставленные строки, строки нумеруются с разбиением по чистому субъекту
c) обновить единственную строку, которую мы делаем, в основном предложении обновления.

вот код...

ALTER TRIGGER [dbo].[UpdateUniqueSubjectAfterInsertUpdate]
   ON  [dbo].[Contents]
   AFTER INSERT,UPDATE
AS
BEGIN
    SET NOCOUNT ON
    
    DECLARE @InsertRows TABLE (ContentId INTEGER PRIMARY KEY,
        ContentTypeId TINYINT,
        CleanedSubject NVARCHAR(300))

    DECLARE @UniqueSubjectRows TABLE (ContentId INTEGER PRIMARY KEY,
    UniqueSubject NVARCHAR(350))
    
DECLARE @UniqueSubjectRows TABLE (ContentId INTEGER PRIMARY KEY,
        UniqueSubject NVARCHAR(350))
        
    -- Grab all the records that have been updated/inserted.
    INSERT INTO @InsertRows(ContentId, ContentTypeId, CleanedSubject)
    SELECT ContentId, ContentTypeId, [dbo].[ToUriCleanText]([Subject])
    FROM INSERTED
    
    
    -- Determine the correct unique subject by using ROW_NUMBER partitioning.
    INSERT INTO @UniqueSubjectRows
    SELECT SubResult.ContentId, UniqueSubject = CASE SubResult.RowNumber 
        WHEN 1 THEN SubResult.CleanedSubject 
        ELSE SubResult.CleanedSubject + CAST(SubResult.RowNumber - 1 AS NVARCHAR(5)) END
    FROM (
        -- Order all the cleaned subjects, partitioned by the cleaned subject.
        SELECT a.ContentId, a.CleanedSubject, ROW_NUMBER() OVER (PARTITION BY a.CleanedSubject ORDER BY a.ContentId) AS RowNumber
        FROM ContentsCleanSubjectView a 
            INNER JOIN @InsertRows b ON a.ContentTypeId = b.ContentTypeId AND a.CleanedSubject = b.CleanedSubject
        GROUP BY a.contentId, a.cleanedSubject
    ) SubResult
    INNER JOIN [dbo].[Contents] c ON c.ContentId = SubResult.ContentId
    INNER JOIN @InsertRows d ON c.ContentId = d.ContentId
    
    -- Now update all the effected rows.
    UPDATE a
    SET a.UniqueSubject = b.UniqueSubject
    FROM [dbo].[Contents] a INNER JOIN @UniqueSubjectRows b ON a.ContentId = b.ContentId
END  

Теперь подзапрос корректно возвращает все вычищенные темы, правильно разбитые на разделы и правильно пронумерованные. Я никогда не знал о команде «РАЗДЕЛ», так что этот трюк был здесь большим ответом :)

Затем я просто присоединился к подзапросу со строкой, которая обновляется в родительском запросе. Номер строки правильный, так что теперь я просто делаю случай. если очищенная тема существует впервые (например, row_number = 1), не изменяйте ее. в противном случае добавьте row_number минус один. Это означает, что 2-й экземпляр одной и той же темы будет иметь уникальную тему => cleansubject + '1'.

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

Так .. это перепроектировано?

Редактировать 1:

Переработан код триггера, чтобы он стал более производительным.

person Pure.Krome    schedule 15.06.2009