为什么我的光标停在循环中间?

时间:2009-06-04 11:01:15

标签: sql-server sql-server-2005 tsql

此处发布的代码是“示例”代码,它不是生产代码。我这样做是为了解决我正在解释的可读/简洁的问题。


使用类似下面的代码,我们遇到了一个奇怪的错误。在每次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

为什么代码会在循环中间退出?

6 个答案:

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