我有一个存储过程,负责一次插入或更新多个记录。为了提高性能,我想在我的存储过程中执行此操作。
此存储过程采用逗号分隔的许可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
如何循环浏览用户定义函数返回的表中的记录,以根据需要动态插入或更新记录?
答案 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)
有许多方法可以完成你要问的部分。
传递值
有很多方法可以做到这一点。以下是一些可以帮助您入门的想法:
Erland Sommarskog对lists in sql server进行了精彩的全面讨论。在我看来,SQL 2008中的表值参数是最优雅的解决方案。
<强>的Upsert /合并强>
重要的问题
然而,没有人提到的一件事是,当存在高并发性时,几乎所有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,则可以使用MERGE。 Here'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