SQL - 一次插入和更新多个记录

时间:2010-02-04 20:55:12

标签: sql-server

我有一个存储过程,负责一次插入或更新多个记录。为了提高性能,我想在我的存储过程中执行此操作。

此存储过程采用逗号分隔的许可ID列表和状态。许可证ID存储在名为@PermitIDs的变量中。状态存储在名为@Status的变量中。我有一个用户定义的函数,将这个以逗号分隔的许可ID列表转换为表。我需要浏览每个ID,并在名为PermitStatus的表中插入或更新。

如果不存在具有许可ID的记录,我想添加记录。如果确实存在,我想用给定的@Status值更新记录。我知道如何为单个ID执行此操作,但我不知道如何为多个ID执行此操作。对于单个ID,我执行以下操作:

-- 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 个答案:

答案 0 :(得分:4)

创建一个split函数,并使用它:

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)

编辑感谢@Emtucifor click here关于竞争条件的提示,我已经在我的答案中包含了锁定提示,以防止在高并发时出现竞争条件问题。< / p>

答案 1 :(得分:4)

有许多方法可以完成你要问的部分。

传递值

有很多方法可以做到这一点。以下是一些可以帮助您入门的想法:

  • 传入一串标识符并将其解析为表格,然后加入。
  • SQL 2008:加入表值参数
  • 期望数据存在于预定义的临时表中并加入
  • 使用会话密钥永久表
  • 将代码放入触发器并加入其中的INSERTED和DELETED表。

Erland Sommarskog对lists in sql server进行了精彩的全面讨论。在我看来,SQL 2008中的表值参数是最优雅的解决方案。

<强>的Upsert /合并

  • 执行单独的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
... 

这将防止并发竞争条件导致错误。

我还建议您在每次数据修改语句后进行错误处理。

答案 2 :(得分:3)

如果您使用的是SQL Server 2008,则可以使用table valued parameters - 将记录表传入存储过程,然后可以执行MERGE

传入表值参数将无需解析CSV字符串。

编辑:
ErikE提出了有关竞争条件的观点,请参阅他的答案和相关文章。

答案 3 :(得分:2)

如果您有SQL Server 2008,则可以使用MERGEHere's an article describing this.

答案 4 :(得分:2)

您应该能够将插入和更新作为两个基于集合的查询。

下面的代码基于我前一段时间写过的数据加载程序,该程序从临时表中获取数据并将其插入或更新到主表中。

我试图让它与你的示例匹配,但你可能需要调整它(并创建一个值为UDF的表来将你的CSV解析为一个id表)。

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

答案 5 :(得分:-1)

基本上你有一个upsert存储过程(例如UpsertSinglePermit)
(就像你上面给出的代码一样)处理一行。

所以我看到的步骤是创建一个新的存储过程(UpsertNPermits)

a)将输入字符串解析为n个记录条目(每个记录包含许可ID和状态) b)上面的Foreach条目,调用UpsertSinglePermit