SQL - вставка и обновление нескольких записей одновременно - PullRequest
6 голосов
/ 04 февраля 2010

У меня есть хранимая процедура, которая отвечает за вставку или обновление нескольких записей одновременно.Я хочу выполнить это в своей хранимой процедуре ради производительности.

Эта хранимая процедура принимает список идентификаторов разрешений и статус, разделенный запятыми.Идентификаторы разрешений хранятся в переменной с именем @PermitIDs.Статус хранится в переменной с именем @Status.У меня есть пользовательская функция, которая преобразует этот разделенный запятыми список идентификаторов разрешений в таблицу.Мне нужно пройти через каждый из этих идентификаторов и выполнить вставку или обновление в таблицу с именем PermitStatus.

Если запись с идентификатором разрешения не существует, я хочу добавить запись.Если он существует, я хочу обновить запись с заданным значением @Status.Я знаю, как сделать это для одного идентификатора, но я не знаю, как это сделать для нескольких идентификаторов.Для одиночных идентификаторов я делаю следующее:

-- Determine whether to add or edit the PermitStatus
DECLARE @count int
SET @count = (SELECT Count(ID) FROM PermitStatus WHERE [PermitID]=@PermitID)

-- If no records were found, insert the record, otherwise add
IF @count = 0
BEGIN
  INSERT INTO
    PermitStatus
  (
    [PermitID],
    [UpdatedOn],
    [Status]
  )
  VALUES
  (
    @PermitID,
    GETUTCDATE(),
    1
  )
  END
  ELSE
    UPDATE
      PermitStatus
    SET
      [UpdatedOn]=GETUTCDATE(),
      [Status]=@Status
    WHERE
      [PermitID]=@PermitID

Как мне циклически проходить по записям в таблице, возвращаемой моей пользовательской функцией, чтобы динамически вставлять или обновлять записи по мере необходимости?

Ответы [ 6 ]

4 голосов
/ 05 февраля 2010

Существуют различные методы для выполнения частей, о которых вы спрашиваете.

Передача значений

Существуют десятки способов сделать это.Вот несколько идей для начала:

  • Передайте строку идентификаторов и разберите ее в таблице, затем присоедините.
  • SQL 2008: присоединение к табличному значениюпараметр
  • Ожидается, что данные будут существовать в предопределенной временной таблице и присоединиться к ней
  • Использовать постоянную таблицу с ключом сеанса
  • Поместить код в триггер и присоединиться к INSERTED.и УДАЛЕННЫЕ таблицы в нем.

Эрланд Соммарског предлагает замечательное всестороннее обсуждение списков на сервере sql .На мой взгляд, табличный параметр в SQL 2008 является наиболее элегантным решением для этого.

Upsert / Merge

  • Выполните отдельное UPDATE и INSERT(два запроса, по одному для каждого набора, а не по строкам).
  • SQL 2008: MERGE.

Важное замечание

Однако, еще одна вещь, о которой никто не упомянул, состоит в том, что почти весь код upsert, , включая SQL 2008 MERGE , страдает от проблем состояния гонки, когда существует высокий параллелизм.Если вы не используете HOLDLOCK и другие подсказки блокировки в зависимости от того, что делается, вы в конечном итоге столкнетесь с конфликтами.Поэтому вам нужно либо блокировать, либо реагировать на ошибки соответствующим образом (некоторые системы с огромными транзакциями в секунду успешно использовали метод ответа на ошибку вместо использования блокировок).

Одна вещь, которую нужно понять, - это то, что различные комбинацииподсказки блокировки неявно изменяют уровень изоляции транзакции, что влияет на то, какой тип блокировки получен.Это меняет все: какие другие блокировки предоставляются (например, простое чтение), время повышения блокировки для обновления с целью обновления и т. Д.

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

Пример кода

CREATE PROCEDURE dbo.PermitStatusUpdate
   @PermitIDs varchar(8000), -- or (max)
   @Status int
AS
SET NOCOUNT, XACT_ABORT ON -- see note below

BEGIN TRAN

DECLARE @Permits TABLE (
   PermitID int NOT NULL PRIMARY KEY CLUSTERED
)

INSERT @Permits
SELECT Value FROM dbo.Split(@PermitIDs) -- split function of your choice

UPDATE S
SET
   UpdatedOn = GETUTCDATE(),
   Status = @Status
FROM
   PermitStatus S WITH (UPDLOCK, HOLDLOCK)
   INNER JOIN @Permits P ON S.PermitID = P.PermitID

INSERT PermitStatus (
   PermitID,
   UpdatedOn,
   Status
)
SELECT
   P.PermitID,
   GetUTCDate(),
   @Status
FROM @Permits P
WHERE NOT EXISTS (
   SELECT 1
   FROM PermitStatus S
   WHERE P.PermitID = S.PermitID
)

COMMIT TRAN

RETURN @@ERROR;

Примечание: XACT_ABORT помогает гарантировать закрытие явной транзакции по истечении времени ожидания или непредвиденной ошибки.

Чтобы убедиться, что это решает проблему блокировки, откройте несколько окон запросов и выполните идентичный пакет следующим образом:

WAITFOR TIME '11:00:00' -- use a time in the near future
EXEC dbo.PermitStatusUpdate @PermitIDs = '123,124,125,126', 1

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

Записи по ссылкам, которые я дал выше, дают даже больше деталей, чем яздесь, а также опишите, что делать с оператором SQL 2008 MERGE.Пожалуйста, внимательно прочитайте их, чтобы по-настоящему понять проблему.

Вкратце, с MERGE явная транзакция не требуется, но вам нужно использовать SET XACT_ABORT ON и использовать подсказку о блокировке:

SET NOCOUNT, XACT_ABORT ON;
MERGE dbo.Table WITH (HOLDLOCK) AS TableAlias
... 

Это предотвратит возникновение ошибок, связанных с гонкой параллелизма.

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

4 голосов
/ 05 февраля 2010

создайте функцию разделения и используйте ее следующим образом:

SELECT
    *
    FROM YourTable  y
    INNER JOIN dbo.splitFunction(@Parameter) s ON y.ID=s.Value

Я предпочитаю подход с таблицей чисел

Чтобы этот метод работал, вам нужно выполнить настройку единовременного расписания:

SELECT TOP 10000 IDENTITY(int,1,1) AS Number
    INTO Numbers
    FROM sys.objects s1
    CROSS JOIN sys.objects s2
ALTER TABLE Numbers ADD CONSTRAINT PK_Numbers PRIMARY KEY CLUSTERED (Number)

После настройки таблицы чисел создайте эту функцию:

CREATE FUNCTION [dbo].[FN_ListToTableAll]
(
     @SplitOn  char(1)      --REQUIRED, the character to split the @List string on
    ,@List     varchar(8000)--REQUIRED, the list to split apart
)
RETURNS TABLE
AS
RETURN 
(

    ----------------
    --SINGLE QUERY-- --this WILL return empty rows
    ----------------
    SELECT
        ROW_NUMBER() OVER(ORDER BY number) AS RowNumber
            ,LTRIM(RTRIM(SUBSTRING(ListValue, number+1, CHARINDEX(@SplitOn, ListValue, number+1)-number - 1))) AS ListValue
        FROM (
                 SELECT @SplitOn + @List + @SplitOn AS ListValue
             ) AS InnerQuery
            INNER JOIN Numbers n ON n.Number < LEN(InnerQuery.ListValue)
        WHERE SUBSTRING(ListValue, number, 1) = @SplitOn

);
GO 

Теперь вы можете легко разбить строку CSV на таблицу и присоединиться к ней:

select * from dbo.FN_ListToTableAll(',','1,2,3,,,4,5,6777,,,')

ВЫВОД:

RowNumber   ListValue
----------- ----------
1           1
2           2
3           3
4           
5           
6           4
7           5
8           6777
9           
10          
11          

(11 row(s) affected)  

Чтобы сделать то, что вам нужно, выполните следующие действия:

--this would be the existing table
DECLARE @OldData  table (RowID  int, RowStatus char(1))

INSERT INTO @OldData VALUES (10,'z')
INSERT INTO @OldData VALUES (20,'z')
INSERT INTO @OldData VALUES (30,'z')
INSERT INTO @OldData VALUES (70,'z')
INSERT INTO @OldData VALUES (80,'z')
INSERT INTO @OldData VALUES (90,'z')


--these would be the stored procedure input parameters
DECLARE @IDList      varchar(500)
       ,@StatusList  varchar(500)
SELECT @IDList='10,20,30,40,50,60'
      ,@StatusList='A,B,C,D,E,F'

--stored procedure local variable
DECLARE @InputList  table (RowID  int, RowStatus char(1))

--convert input prameters into a table
INSERT INTO @InputList
        (RowID,RowStatus)
    SELECT
        i.ListValue,s.ListValue
        FROM dbo.FN_ListToTableAll(',',@IDList)            i
            INNER JOIN dbo.FN_ListToTableAll(',',@StatusList)  s ON i.RowNumber=s.RowNumber

--update all old existing rows
UPDATE o
    SET RowStatus=i.RowStatus
    FROM @OldData               o WITH (UPDLOCK, HOLDLOCK) --to avoid race condition when there is high concurrency as per @emtucifor
        INNER JOIN @InputList   i ON o.RowID=i.RowID

--insert only the new rows
INSERT INTO @OldData
        (RowID, RowStatus)
    SELECT
        i.RowID, i.RowStatus
        FROM @InputList               i
            LEFT OUTER JOIN @OldData  o ON i.RowID=o.RowID
        WHERE o.RowID IS NULL

--display the old table
SELECT * FROM @OldData order BY RowID

ВЫВОД:

RowID       RowStatus
----------- ---------
10          A
20          B
30          C
40          D
50          E
60          F
70          z
80          z
90          z

(9 row(s) affected)

РЕДАКТИРОВАТЬ благодаря @Emtucifor нажмите здесь для получения подсказки о состоянии гонки, я включил подсказки блокировки в свой ответ, чтобы предотвратить проблемы состояния гонки при высоком параллелизме .

3 голосов
/ 05 февраля 2010

Если вы используете SQL Server 2008, вы можете использовать табличные параметры - вы передаете таблицу записей в хранимую процедуру, а затем можете сделать MERGE .

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

Edit:
ErikE поднял вопрос об условиях гонки, пожалуйста, обратитесь к его ответу и связанным статьям.

2 голосов
/ 05 февраля 2010

Вы должны быть в состоянии выполнить вставку и обновление как два набора запросов.

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

Я пытался сделать так, чтобы он соответствовал вашему примеру, но вам, возможно, потребуется настроить его (и создать UDF с табличным значением для анализа вашего CSV в таблице идентификаторов).

-- Update where the join on permitstatus matches
Update
    PermitStatus
Set 
    [UpdatedOn]=GETUTCDATE(),
    [Status]=staging.Status
From 
    PermitStatus status
Join   
    StagingTable staging
On
    staging.PermitId = status.PermitId

-- Insert the new records, based on the Where Not Exists      
Insert 
    PermitStatus(Updatedon, Status, PermitId)
Select (GETUTCDATE(), staging.status, staging.permitId
From 
     StagingTable staging
Where Not Exists
(
    Select 1 from PermitStatus status
    Where status.PermitId = staging.PermidId 
)   
2 голосов
/ 05 февраля 2010

Если у вас SQL Server 2008, вы можете использовать MERGE . Вот статья, описывающая это.

0 голосов
/ 05 февраля 2010

По сути, у вас есть хранимая процедура upsert (например, UpsertSinglePermit)
(как код, который вы дали выше) для работы с одной строкой.

Итак, шаги, которые я вижу, это создать новую хранимую процедуру (UpsertNPermits), которая делает

a) Разберите входную строку на n записей (каждая запись содержит идентификатор разрешения и статус) б) для каждой записи выше, вызовите UpsertSinglePermit

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