使用类似下面的代码,我们遇到了一个奇怪的错误。在每次INSERT之后,WHILE循环停止。
table 包含100行,当插入在50行之后完成时,光标停止,只触及前50行。当插入在55之后完成时它在55之后停止,依此类推。
-- This code is an hypothetical example written to express
-- an problem seen in production
DECLARE @v1 int
DECLARE @v2 int
DECLARE MyCursor CURSOR FAST_FORWARD FOR
SELECT Col1, Col2
FROM table
OPEN MyCursor
FETCH NEXT FROM MyCursor INTO @v1, @v2
WHILE(@@FETCH_STATUS=0)
BEGIN
IF(@v1>10)
BEGIN
INSERT INTO table2(col1) VALUES (@v2)
END
FETCH NEXT FROM MyCursor INTO @v1, @v2
END
CLOSE MyCursor
DEALLOCATE MyCursor
table2 上有一个AFTER INSERT触发器,用于将 table2 上的变量记录到第三个表中,恰当地命名为 mutation 。这包含一个插入来处理插入的游标(以非常特定的方式记录每列的突变,这需要游标)。
一些背景知识:这存在于一组小支持表中。为了审计目的,项目要求记录对源数据所做的每个更改。带有日志记录的表格包含诸如银行帐号之类的内容,其中将存入大量资金。最多只有几千条记录,它们只应该很少修改。审计功能可以阻止欺诈行为:因为我们记录了“改变了什么”与“谁做了”。
实现此目的的明显,快速和合理的方法是每次更新时存储整行。然后我们不需要光标,它会更好地执行一个因素。然而,情况的政治意味着我的双手被束缚。
呼。现在回到问题。
触发器的简化版本(真实版本每列插入一次,并且还插入旧值):
--This cursor is an hypothetical cursor written to express
--an problem seen in production.
--On UPDATE a new record must be added to table Mutaties for
--every row in every column in the database. This is required
--for auditing purposes.
--An set-based approach which stores the previous state of the row
--is expressly forbidden by the customer
DECLARE @col1 int
DECLARE @col2 int
DECLARE @col1_old int
DECLARE @col2_old int
--Loop through old values next to new values
DECLARE MyTriggerCursor CURSOR FAST_FORWARD FOR
SELECT i.col1, i.col2, d.col1 as col1_old, d.col2 as col2_old
FROM Inserted i
INNER JOIN Deleted d ON i.id=d.id
OPEN MyTriggerCursor
FETCH NEXT FROM MyTriggerCursor INTO @col1, @col2, @col1_old, @col2_old
--Loop through all rows which were updated
WHILE(@@FETCH_STATUS=0)
BEGIN
--In production code a few more details are logged, such as userid, times etc etc
--First column
INSERT Mutaties (tablename, columnname, newvalue, oldvalue)
VALUES ('table2', 'col1', @col1, @col1_old)
--Second column
INSERT Mutaties (tablename, columnname, newvalue, oldvalue)
VALUES ('table2', 'col2', @col2, @col1_old)
FETCH NEXT FROM MyTriggerCursor INTO @col1, @col2, @col1_old, @col2_old
END
CLOSE MyTriggerCursor
DEALLOCATE MyTriggerCursor
为什么代码会在循环中间退出?
答案 0 :(得分:9)
你的问题是你根本不应该使用光标!这是上面给出的例子的代码。
INSERT INTO table2(col1)
SELECT Col1 FROM table
where col1>10
你也永远不应该在触发器中使用光标,这会破坏性能。如果有人在插入中添加了100,000行,则可能需要几分钟(甚至几小时)而不是毫秒或秒。我们在这里替换了一个(在我接受这个工作之前)并且从40分钟到45秒减少了对该表的导入。
应检查使用游标的任何生产代码,以使用正确的基于集合的代码替换它。根据我的经验,90%以上的游标可以以基于集合的方式重新编写。
答案 1 :(得分:4)
这是对触发器的简单误解......你根本不需要光标
if UPDATE(Col1)
begin
insert into mutaties
(
tablename,
columnname,
newvalue
)
select
'table2',
coalesce(d.Col1,''),
coalesce(i.Col1,''),
getdate()
from inserted i
join deleted d on i.ID=d.ID
and coalesce(d.Col1,-666)<>coalesce(i.Col1,-666)
end
这个代码的基本操作是它检查该列的数据是否已更新。如果是,它会比较新旧数据,如果它不同,则会插入到日志表中。
你是第一个代码示例可以很容易地用这样的东西替换
insert into table2 (col1)
select Col2
from table
where Col1>10
答案 2 :(得分:2)
Ryan,您的问题是@@ FETCH_STATUS对于连接中的所有游标都是全局的。
因此触发器中的光标以@@ FETCH_STATUS为-1结束。当控制返回上面的代码时,最后的@@ FETCH_STATUS为-1,因此光标结束。
文档中对此进行了解释,该文档可在MSDN here上找到。
您可以使用局部变量来存储@@ FETCH_STATUS,并将该局部变量放入循环中。所以你得到这样的东西:
DECLARE @v1 int
DECLARE @v2 int
DECLARE @FetchStatus int
DECLARE MyCursor CURSOR FAST_FORWARD FOR
SELECT Col1, Col2
FROM table
OPEN MyCursor
FETCH NEXT FROM MyCursor INTO @v1, @v2
SET @FetchStatus = @@FETCH_STATUS
WHILE(@FetchStatus=0)
BEGIN
IF(@v1>10)
BEGIN
INSERT INTO table2(col1) VALUES (@v2)
END
FETCH NEXT FROM MyCursor INTO @v1, @v2
SET @FetchStatus = @@FETCH_STATUS
END
CLOSE MyCursor
DEALLOCATE MyCursor
值得注意的是,此行为不适用于嵌套游标。我做了一个简单的例子,它在SqlServer 2008上返回预期的结果(50)。
USE AdventureWorks
GO
DECLARE @LocationId smallint
DECLARE @ProductId smallint
DECLARE @Counter int
SET @Counter=0
DECLARE MyFirstCursor CURSOR FOR
SELECT TOP 10 LocationId
FROM Production.Location
OPEN MyFirstCursor
FETCH NEXT FROM MyFirstCursor INTO @LocationId
WHILE (@@FETCH_STATUS=0)
BEGIN
DECLARE MySecondCursor CURSOR FOR
SELECT TOP 5 ProductID
FROM Production.Product
OPEN MySecondCursor
FETCH NEXT FROM MySecondCursor INTO @ProductId
WHILE(@@FETCH_STATUS=0)
BEGIN
SET @Counter=@Counter+1
FETCH NEXT FROM MySecondCursor INTO @ProductId
END
CLOSE MySecondCursor
DEALLOCATE MySecondCursor
FETCH NEXT FROM MyFirstCursor INTO @LocationId
END
CLOSE MyFirstCursor
DEALLOCATE MyFirstCursor
--
--Against the initial version of AdventureWorks, counter should be 50.
--
IF(@Counter=50)
PRINT 'All is good with the world'
ELSE
PRINT 'Something''s wrong with the world today'
答案 3 :(得分:1)
此代码不会从游标中获取任何其他值,也不会增加任何值。实际上,没有理由在这里实现游标。
您的整个代码可以重写为:
DECLARE @v1 int
DECLARE @v2 int
SELECT @v1 = Col1, @v2 = Col2
FROM table
IF(@v1>10)
INSERT INTO table2(col1) VALUES (@v2)
编辑:已修改帖子以解决我所指的问题。
答案 4 :(得分:1)
您不必使用游标将每列作为单独的行插入。
以下是一个例子:
INSERT LOG.DataChanges
SELECT
SchemaName = 'Schemaname',
TableName = 'TableName',
ColumnName = CASE ColumnID WHEN 1 THEN 'Column1' WHEN 2 THEN 'Column2' WHEN 3 THEN 'Column3' WHEN 4 THEN 'Column4' END
ID = Key1,
ID2 = Key2,
ID3 = Key3,
DataBefore = CASE ColumnID WHEN 1 THEN I.Column1 WHEN 2 THEN I.Column2 WHEN 3 THEN I.Column3 WHEN 4 THEN I.Column4 END,
DataAfter = CASE ColumnID WHEN 1 THEN D.Column1 WHEN 2 THEN D.Column2 WHEN 3 THEN D.Column3 WHEN 4 THEN D.Column4 END,
DateChange = GETDATE(),
USER = WhateverFunctionYouAreUsingForThis
FROM
Inserted I
FULL JOIN Deleted D ON I.Key1 = D.Key1 AND I.Key2 = D.Key2
CROSS JOIN (
SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4
) X (ColumnID)
在X表中,您可以使用第二列编写其他行为,该列专门描述了如何处理该列(假设您希望一些人一直发布,但只有在值发生变化时才会发布)。重要的是,这是将行拆分为每列的交叉连接技术的示例,但还有很多工作要做。请注意,完整连接允许此操作处理插入和删除以及更新。
我也完全同意存储每一行都是FAR优越的。有关详情,请参阅this forum。
答案 5 :(得分:0)
如ck所述,您不会获取任何其他值。因此,@@ FETCH_STATUS从AFTER INSERT触发器中包含的游标中获取其值。
您应该将代码更改为
DECLARE @v1 int
DECLARE @v2 int
DECLARE MyCursor CURSOR FAST_FORWARD FOR
SELECT Col1, Col2
FROM table
OPEN MyCursor
FETCH NEXT FROM MyCursor INTO @v1, @v2
WHILE(@@FETCH_STATUS=0)
BEGIN
IF(@v1>10)
BEGIN
INSERT INTO table2(col1) VALUES (@v2)
END
FETCH NEXT FROM MyCursor INTO @v1, @v2
END