Как реализовать условную хранимую процедуру Upsert? - PullRequest
6 голосов
/ 10 июля 2009

Я пытаюсь реализовать ваши основные функциональные возможности UPSERT, но с изюминкой: иногда я не хочу фактически обновлять существующую строку.

По сути, я пытаюсь синхронизировать некоторые данные между различными репозиториями, и функция Upsert показалась мне подходящей. Поэтому, основываясь в основном на ответе Сэма Шафрона на этот вопрос , а также на некоторых других исследованиях и чтениях, я придумал эту хранимую процедуру:

(примечание: я использую MS SQL Server 2005, поэтому оператор MERGE не подходит)

CREATE PROCEDURE [dbo].[usp_UpsertItem] 
    -- Add the parameters for the stored procedure here
    @pContentID varchar(30) = null, 
    @pTitle varchar(255) = null,
    @pTeaser varchar(255) = null 
AS
BEGIN
    -- SET NOCOUNT ON added to prevent extra result sets from
    -- interfering with SELECT statements.
    SET NOCOUNT ON;

    BEGIN TRANSACTION

        UPDATE dbo.Item WITH (SERIALIZABLE)
        SET Title = @pTitle,
            Teaser = @pTeaser
        WHERE ContentID = @pContentID

        IF @@rowcount = 0
            INSERT INTO dbo.Item (ContentID, Title, Teaser)
            VALUES (@pContentID, @pTitle, @pTeaser)

    COMMIT TRANSACTION
END

Мне это удобно для базового Upsert, но я бы хотел, чтобы фактическое обновление зависело от значения другого столбца. Думайте об этом как о «блокировании» строки, чтобы дальнейшие обновления не выполнялись процедурой Upsert. Я мог бы изменить выражение UPDATE следующим образом:

UPDATE dbo.Item WITH (SERIALIZABLE)
SET Title = @pTitle,
    Teaser = @pTeaser
WHERE ContentID = @pContentID
AND RowLocked = false

Но тогда последующая вставка завершится неудачно с уникальным нарушением ограничения (для поля ContentID), когда она попытается вставить строку, которая уже существует, но не обновлена, поскольку она была «заблокирована».

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

Ответы [ 6 ]

9 голосов
/ 10 июля 2009

Очень распространенная проблема. Некоторые подходы не поддерживают высокий параллелизм. Описано и стресс-тестирование здесь:

Стресс-тестирование UPSERT

Защитное программирование базы данных: устранение операторов IF.

В таких случаях недостаточно просто написать какой-то код, вам нужно выставить его к высокому параллелизму. Например, я не уверен, что понял, что такое CptSkippy рекомендует, но следующее демонстрирует, как провести стресс-тест. Настройте таблицу и процедуру:

CREATE TABLE [dbo].[TwoINTs](
      [ID] [int] NOT NULL,
      [i1] [int] NOT NULL,
      [i2] [int] NOT NULL,
      [i3] [int] NOT NULL
);
CREATE PROCEDURE dbo.SaveTwoINTs(@ID INT, @i1 INT, @i2 INT)
AS
BEGIN
      SET NOCOUNT ON;
      SET XACT_ABORT OFF;
      SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
      DECLARE @ret INT;
      SET @ret=0;
      BEGIN TRAN; 
IF EXISTS(SELECT 1 FROM dbo.TwoINTs WHERE ID=@ID) BEGIN
      UPDATE dbo.TwoINTs WITH (SERIALIZABLE)
         SET i1=i1+@i1, i2=i2+@i2 WHERE ID=@ID;
      SET @ret=@@ERROR;
END ELSE BEGIN
     INSERT INTO dbo.TwoINTs(ID, i1, i2, i3)VALUES(@ID, @i1, @i2, @i1);
      SET @ret=@@ERROR;
END;
COMMIT;
RETURN @ret;
END
GO

Установите два цикла, которые выполняют эту процедуру:

CREATE PROCEDURE Testers.UpsertLoop1
AS
BEGIN
DECLARE @ID INT, @i1 INT, @i2 INT, @count INT, @ret INT;
SET @count = 0;
WHILE @count<50000 BEGIN
      SELECT @ID = COALESCE(MAX(ID),0) + 1 FROM dbo.TwoInts;
    EXEC @ret=dbo.SaveTwoINTs @ID, 1, 0;
      SET @count = @count + 1;
END;
END;
GO
CREATE PROCEDURE Testers.UpsertLoop2
AS
BEGIN
DECLARE @ID INT, @i1 INT, @i2 INT, @count INT, @ret INT;
SET @count = 0;
WHILE @count<50000 BEGIN
      SELECT @ID = COALESCE(MAX(ID),0) + 1 FROM dbo.TwoInts;
    EXEC @ret=dbo.SaveTwoINTs @ID, 0, 1;
      SET @count = @count + 1;
END;
END;

Выполните эти процедуры в двух вкладках и убедитесь, что вы получаете много ошибок:

Testers.UpsertLoop1 --run in one tab
Testers.UpsertLoop1 --run in one tab

Msg 2601, Level 14, State 1, Procedure SaveTwoINTs, Line 15
Cannot insert duplicate key row in object 'dbo.TwoINTs' with unique index 'UNQ_TwoInts_ID'.
The statement has been terminated.

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

2 голосов
/ 10 июля 2009

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

/*
CREATE TABLE Item
 (
   Title      varchar(255)  not null
  ,Teaser     varchar(255)  not null
  ,ContentId  varchar(30)  not null
  ,RowLocked  bit  not null
)


UPDATE item
 set RowLocked = 1
 where ContentId = 'Test01'

*/


DECLARE
  @Check varchar(30)
 ,@pContentID varchar(30)
 ,@pTitle varchar(255)
 ,@pTeaser varchar(255)

set @pContentID = 'Test01'
set @pTitle     = 'TestingTitle'
set @pTeaser    = 'TestingTeasier'

set @check = null

UPDATE dbo.Item
 set
   @Check = ContentId
  ,Title  = @pTitle
  ,Teaser = @pTeaser
 where ContentID = @pContentID
  and RowLocked = 0

print isnull(@check, '<check is null>')

IF @Check is null
    INSERT dbo.Item (ContentID, Title, Teaser, RowLocked)
     values (@pContentID, @pTitle, @pTeaser, 0)

select * from Item

Хитрость в том, что вы можете устанавливать значения в локальных переменных в операторе Update. Выше значение «flag» устанавливается только в том случае, если обновление работает (то есть критерии обновления выполнены); в противном случае, оно не будет изменено (здесь, в нуле), вы можете проверить это и обработать соответствующим образом.

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

- Дополнения, продолжение второго комментария ниже -----------

г. Идеи Saffron - это тщательный и надежный способ реализации этой процедуры, поскольку ваши первичные ключи определяются снаружи и передаются в базу данных (т. Е. Вы не используете столбцы идентификаторов - прекрасно, часто они чрезмерно используются).

Я провел еще несколько тестов (добавил ограничение первичного ключа для столбца ContentId, обернул UPDATE и INSERT в транзакцию, добавил в обновление сериализуемую подсказку) и да, это должно делать все, что вы хотите. Неудачное обновление перекрывает блокировку диапазона в этой части индекса, что блокирует любые одновременные попытки вставить это новое значение в столбец. Конечно, если N запросов подано одновременно, «first» создаст строку, и она будет немедленно обновлена ​​вторым, третьим и т. Д., Если вы не установите «lock» где-нибудь вдоль линии. Хороший трюк!

(Обратите внимание, что без индекса для ключевого столбца вы заблокируете всю таблицу. Кроме того, блокировка диапазона может заблокировать строки по обе стороны от нового значения - или, возможно, они не будут, я не проверял это. Не должно иметь значения, поскольку продолжительность операции должна быть [?] в миллисекундах с одной цифрой.)

1 голос
/ 10 июля 2009
BEGIN TRANSACTION

IF EXISTS(SELECT 1 FROM dbo.Item WHERE ContentID = @pContentID)
     UPDATE dbo.Item WITH (SERIALIZABLE)
     SET Title = @pTitle, Teaser = @pTeaser
     WHERE ContentID = @pContentID
     AND RowLocked = false
ELSE
     INSERT INTO dbo.Item
          (ContentID, Title, Teaser)
     VALUES
          (@pContentID, @pTitle, @pTeaser)

COMMIT TRANSACTION
0 голосов
/ 17 сентября 2013

Я бы отбросил транзакцию.

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

Просто сделайте проверку Exists (). В любом случае вы должны пройти через стол, поэтому скорость не имеет значения.

Насколько я вижу, транзакция не нужна.

0 голосов
/ 10 июля 2009

CREATE PROCEDURE [dbo]. [Usp_UpsertItem] - Добавьте параметры для хранимой процедуры здесь @pContentID varchar (30) = ноль, @pTitle varchar (255) = ноль, @pTeaser varchar (255) = ноль КАК НАЧАТЬ - SET NOCOUNT ON добавлен для предотвращения дополнительных наборов результатов - вмешательство в операторы SELECT. SET NOCOUNT ON;

BEGIN TRANSACTION
    IF EXISTS (SELECT 1 FROM dbo.Item WHERE ContentID = @pContentID
             AND RowLocked = false)
       UPDATE dbo.Item 
       SET Title = @pTitle, Teaser = @pTeaser
       WHERE ContentID = @pContentID
             AND RowLocked = false
    ELSE IF NOT EXISTS (SELECT 1 FROM dbo.Item WHERE ContentID = @pContentID)
            INSERT INTO dbo.Item (ContentID, Title, Teaser)
            VALUES (@pContentID, @pTitle, @pTeaser)

COMMIT TRANSACTION

END

0 голосов
/ 10 июля 2009

Вы можете изменить порядок обновления / вставки. Таким образом, вы делаете вставку в try / catch, и если вы получаете нарушение ограничения, тогда производите обновление. Хотя он немного грязный.

...