我使用下面的代码插入数据并避免SQL Server中的重复行。 它在测试环境(本地SQL Server)中对我来说效果很好,但是在工作环境(远程SQL服务器)中工作了几天之后,我在表中找到了几个重复的行。我想知道它是如何可能的?主要问题我该如何调试这个问题?也许SQL Server中有一些日志显示已执行命令的历史记录? 任何建议都表示赞赏!
SQLcmd = _
"IF ( NOT EXISTS ( SELECT * FROM " & TableName & " WHERE" & _
" SSYS_ID = " & SmartTags( "SSYS_ID" ) & _
" AND TASK_ID = " & SmartTags( "TASK_ID" ) & _
" AND COPM_ID = " & SmartTags( "COPM_ID" ) & _
" AND SILAGE_ID = " & SmartTags( "SILAGE_ID" ) & _
" AND WCELL_ID = " & SmartTags( "WCELL_ID" ) & _
" ) ) " & _
" BEGIN" & _
" INSERT INTO " & TableName & _
"([SSYS_ID]" & _
",[TASK_ID]" & _
",[COPM_ID]" & _
",[SILAGE_ID]" & _
",[RECIPE_ID]" & _
",[WCELL_ID]" & _
"VALUES (" & _
" " & SmartTags( "SSYS_ID" ) & _
"," & SmartTags( "TASK_ID" ) & _
"," & SmartTags( "COPM_ID" ) & _
"," & SmartTags( "SILAGE_ID" ) & _
"," & SmartTags( "RECIPE_ID" ) & _
"," & SmartTags( "WCELL_ID" ) & _
")" & _
" END"
conn.Execute SQLcmd, RecordsAffected, adExecuteNoRecords
答案 0 :(得分:2)
首先,我同意上面关于sql注入的声明。您应该切换到参数化查询。
其次,这不是线程安全的。 2个线程可能会尝试同时插入相同的值。两个线程都进行IF
检查,但没有找到匹配项,然后都插入。这听起来像是在大批量生产环境中发生的事情。您需要在单个语句中执行它,例如MERGE
DECLARE
@SSYS_ID INTEGER
, @TASK_ID INTEGER
, @COPM_ID INTEGER
, @SILAGE_ID INTEGER
, @RECIPE_ID INTEGER
, @WCELL_ID INTEGER
MERGE TABLENAME AS target
USING
(
SELECT
@SSYS_ID AS SSYS_ID
, @TASK_ID AS TASK_ID
, @COPM_ID AS COPM_ID
, @SILAGE_ID AS SILAGE_ID
, @RECIPE_ID AS RECIPE_ID
, @WCELL_ID AS WCELL_ID
) AS source
ON
(
target.SSYS_ID = source.SSYS_ID
AND target.TASK_ID = source.TASK_ID
AND target.COPM_ID = source.COPM_ID
AND target.SILAGE_ID = source.SILAGE_ID
AND target.WCELL_ID = source.WCELL_ID
)
WHEN NOT MATCHED THEN
INSERT (SSYS_ID, TASK_ID, COPM_ID, SILAGE_ID, WCELL_ID)
VALUES (source.SSYS_ID, source.TASK_ID, source.COPM_ID, source.SILAGE_ID, source.WCELL_ID)
;
答案 1 :(得分:1)
您所描述的问题表明多个用户同时尝试同时将相同的数据插入表中(即SSYS_ID
,TASK_ID
,COPM_ID
, SILAGE_ID
和WCELL_ID
)。在单用户开发/测试环境中,作为唯一用户,您测试代码时问题无法浮现(或难以重现)。在将应用程序部署到多用户生产环境之后问题表现出来的事实可能表明存在大量并发用户将数据插入表中,因此多个用户尝试在该表中插入相同内容的概率同一时间很高。因此,您应该仔细选择不会妨碍数据库性能的解决方案。
例如,假设有两个用户(即交易):用户A和用户B.同时两个用户都开始插入相同的数据。因此,可能会发生以下情况:
User A
和User B
两者都开始插入相同的数据User A
检查表中是否已存在包含该数据的行。数据不存在,因此User A
继续插入数据。User B
检查表中是否已存在具有相同数据的行。数据仍然不存在,因此User B
也会继续插入数据。User A
继续并插入数据。User B
会继续并插入相同的数据您可以使用以下几个选项来解决此问题:
我建议第一个选项,考虑到大量并发用户,只需通过在列上创建唯一约束来强制执行在数据库级别拥有唯一数据的业务规则({ {1}},SSYS_ID
,TASK_ID
,COPM_ID
,SILAGE_ID
)。在这种情况下,您的代码应该只有WCELL_ID
语句:
INSERT
您应该修改代码以检查是否发生了数据库错误,特别是错误2627,而不是检查SQLcmd = _
" INSERT INTO " & TableName & _
"([SSYS_ID]" & _
",[TASK_ID]" & _
",[COPM_ID]" & _
",[SILAGE_ID]" & _
",[RECIPE_ID]" & _
",[WCELL_ID]" & _
"VALUES (" & _
" " & SmartTags( "SSYS_ID" ) & _
"," & SmartTags( "TASK_ID" ) & _
"," & SmartTags( "COPM_ID" ) & _
"," & SmartTags( "SILAGE_ID" ) & _
"," & SmartTags( "RECIPE_ID" ) & _
"," & SmartTags( "WCELL_ID" ) & _
")"
conn.Execute SQLcmd, RecordsAffected, adExecuteNoRecords
以查看是否插入了行。(如果您使用改为使用唯一索引,检查错误2601;请参阅How to troubleshoot Error 2601 Cannot insert duplicate key row in object '%.*ls' with unique index '%.*ls'. The duplicate key value is %ls.)。
此解决方案不应像下面的第二个解决方案那样损害数据库性能。它会导致插入和更新的性能降低,从而维护唯一索引。它还将在数据库级别强制执行规则,以便无论使用哪种SQL语句插入行,都将保证将拒绝重复的行。这是今天和明天的设计。
第二个选项是向RecordsAffected
语句添加表提示,以检查是否已有行。因此,代码应如下所示:
SELECT
请注意IF (NOT EXISTS(
SELECT *
FROM YourTable WITH(UPDLOCK, HOLDLOCK)
WHERE
SSYS_ID = @SSYS_ID
AND TASK_ID = @TASK_ID
AND COPM_ID = @COPM_ID
AND SILAGE_ID = @SILAGE_ID
AND WCELL_ID = @WCELL_ID
))
BEGIN
INSERT INTO YourTable(
SSYS_ID
,TASK_ID
,COPM_ID
,SILAGE_ID
,RECIPE_ID
,WCELL_ID
) VALUES (
@SSYS_ID
,@TASK_ID
,@COPM_ID
,@SILAGE_ID
,@RECIPE_ID
,@WCELL_ID
)
END
和UPDLOCK
。他们锁定表的目的是更新它直到事务结束,在你的情况下应该直到ADO命令完成。因此,HOLDLOCK
锁定表,检查表中是否已有行,然后命令继续(如有必要)插入行,导致其他并发命令等待SELECT
s直到插入完成,命令完成。例如,事件流可能如下所示:
SELECT
和User A
两者都开始插入相同的数据User B
锁定表并检查是否已存在包含该数据的行User A
等待表格解锁以继续检查User B
会插入数据。User A
的命令(事务)完成,表格解锁User A
会锁定表格并检查表格中是否有一行User B
不会插入该重复行User B
的命令(事务)完成,表格解锁请谨慎使用此解决方案,并且仅在您的情况下无法创建唯一约束或唯一索引时,因为它会因锁定表而降低数据库性能。这些命令(lock-check-then-insert)将阻止表上的所有其他操作:其他插入,更新以及具有更新意图的其他选择。此外,如果您在代码中显式启动事务(ADO的User B
),请确保在任何情况下提交(ADO的BeginTrans
)或回滚(ADO的CommitTrans
)事务(定期或在异常/错误处理程序中),否则一个未终止的事务将继续保持表上的锁定阻止表上的所有其他进一步操作。
另外两个选项基本上只是前一个选项的变体:RollbackTrans
和MERGE
。除非使用相同的表提示修改它们,否则它们都不能解决并发问题。因此,使用表提示,性能会因先前的解决方案中的锁定和阻塞而受到影响。他们是:
INSERT INTO SELECT
与MERGE
:
HOLDLOCK
请注意,它只有MERGE YourTable WITH (HOLDLOCK) AS dst
USING (
SELECT
@SSYS_ID AS SSYS_ID
,@TASK_ID AS TASK_ID
,@COPM_ID AS COPM_ID
,@SILAGE_ID AS SILAGE_ID
,@WCELL_ID AS WCELL_ID
) AS src
ON dst.SSYS_ID = src.SSYS_ID AND
dst.TASK_ID = src.TASK_ID AND
dst.COPM_ID = src.COPM_ID AND
dst.SILAGE_ID = src.SILAGE_ID AND
dst.WCELL_ID = src.WCELL_ID
WHEN NOT MATCHED THEN
INSERT (
SSYS_ID
,TASK_ID
,COPM_ID
,SILAGE_ID
,RECIPE_ID
,WCELL_ID
) VALUES (
@SSYS_ID
,@TASK_ID
,@COPM_ID
,@SILAGE_ID
,@RECIPE_ID
,@WCELL_ID
);
。它不需要HOLDLOCK
,因为它自己发出更新锁。
UPDLOCK
与INSERT INTO SELECT
和UPDLOCK
:
HOLDLOCK
由于服务器负载较高且响应速度较慢,查询时间较长或有时仅因为连接速度较慢,可能会发生超时。它发生在客户端,而不是服务器上。当它发生时,客户端通知服务器有关超时的信息,然后服务器在其决定的最佳时间结束当前正在执行的语句,但它不会回滚事务(除非INSERT INTO YourTable(
SSYS_ID
,TASK_ID
,COPM_ID
,SILAGE_ID
,RECIPE_ID
,WCELL_ID
)
SELECT
@SSYS_ID
,@TASK_ID
,@COPM_ID
,@SILAGE_ID
,@RECIPE_ID
,@WCELL_ID
WHERE
NOT EXISTS(
SELECT *
FROM YourTable WITH (UPDLOCK, HOLDLOCK)
WHERE
SSYS_ID = @SSYS_ID
AND TASK_ID = @TASK_ID
AND COPM_ID = @COPM_ID
AND SILAGE_ID = @SILAGE_ID
AND WCELL_ID = @WCELL_ID
)
为XACT_ABORT
})。
在您的情况下,可能由于服务器负载较高,在插入完成后以及事务仍在提交(自动)时发生超时。然后,当您立即重新触发该命令时,它会设法执行第一个ON
语句,该语句在上一个命令的事务仍在提交时检查该行是否存在。
要防止这种情况发生,您应该在代码中显式启动事务,调用命令,然后在命令成功时提交事务,或者在超时时回滚事务然后重试。请注意,当服务器负载很高或事务很大并且需要时间提交或回滚时,提交或回滚事务也会超时,但超时不会导致提交或回滚被停止或撤消 - 他们总是成功。
SELECT
),执行命令(BeginTrans
),然后在成功时提交事务(Execute
)或在失败时回滚它(CommitTrans
})。希望它有所帮助。