2010-02-04 7 views
6

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

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

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

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

-- 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][email protected] 
    WHERE 
     [PermitID][email protected] 

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

+0

Какую версию SQL Server вы используете? – AdaTheDev

+1

Вы можете почти наверняка сделать это как действие, основанное на наборе (нет необходимости в цикле) - как Ада спрашивает, на какой версии SQL Server вы работаете? –

+0

Кстати, user255048, какие это разрешения? Просто из любопытства. :) – ErikE

ответ

4

создать раздельную функцию, и использовать его как:

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

I prefer the number table approach

Для работы этого метода, вы должны сделать это один раз настройки таблицы:

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) 

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

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) 

EDIT спасибо @Emtucifor click here за подсказку о состоянии гонки, я включил в мой ответ замечания о блокировке, чтобы предотвратить проблемы с состоянием гонки, когда есть высокий уровень параллелизма.

+0

Вы не указали условия гонки и блокировки, разбивающие ваш 2-компонентный UPSERT. Я думаю, это очень важно. – ErikE

+0

@ Emtucifor, я включил некоторые блокирующие подсказки, чтобы предотвратить какие-либо условия гонки –

2

Если у вас есть SQL Server 2008, вы можете использовать MERGE. Here's an article describing this.

+0

Вы не указали условия гонки и блокировки, нарушающие MERGE. Я думаю, это очень важно. – ErikE

-1

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

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

а) Разобрать строку ввода в п записи записей (каждая запись содержит разрешение идентификатор и статус) б) запись Foreach в выше , invets UpsertSinglePermit

+0

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

+0

очень плохой метод. используйте не зависящую от набора логику. – HLGEM

3

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

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

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

+0

, независимо от версии SQL Server, использование табличных параметров зависит от поддержки этого языка приложения. Но OP не упомянул версию SQL Server или язык и версию приложения, которые они используют. –

+1

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

+0

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

2

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

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

Я попытался привести его в соответствие с вашим примером, но вам может потребоваться настроить его (и создать таблицу с оценкой 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 
) 
+1

Одна заметка выше - с SQL Server 2008 вы можете достичь аналогичной логики с помощью слияния –

+0

Вы не указали условия гонки и блокировки, разбивающие вашу 2-часть UPSERT. Я думаю, это очень важно. – ErikE

4

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

Попутный Значения

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

  • Передайте строку идентификаторов и проанализируйте ее в таблицу, затем присоедините.
  • SQL 2008: Присоединяйтесь к параметру табличного
  • Expect данные существуют в предопределенных временных таблицах и присоединиться к нему
  • Используйте сессионную манипулированный постоянная таблицу
  • Поместите код в триггере и присоединиться к таблицам INSERTED и DELETED.

Erland Sommarskog предлагает замечательную всестороннюю дискуссию lists in sql server. На мой взгляд, параметр table-value в 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 helps guarantee the explicit transaction is closed после тайм-аута или неожиданной ошибки.

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

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 
... 

Это предотвратит условия параллельности гонки может привести к ошибкам.

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

+0

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

+0

@KM: Вы правы, мой пост не хватало в этой области. Я обновил его. Есть ли другой способ улучшить его, чтобы заработать хотя бы нейтральное голосование? – ErikE

+1

@ Emtucifor - оцените отзывы о моем ответе. Я обновляю ссылку на ваш ответ по этому вопросу. Тем не менее, было немного суровым ответить на вопрос, основанный на этой единственной точке, поскольку это подразумевает, что ответы были технически неправильными или вводящими в заблуждение. – AdaTheDev

Смежные вопросы