閱讀613 返回首頁    go 阿裏雲 go 技術社區[雲棲]


逐行處理數據時避免死循環

當在一個循環體內逐行處理數據時,你必須確保該循環不會無休止的執行下去。在此,我將描述三個死循環的場景。已經有很多論文論及循環和基於集合的解決方式之間可行性的優劣,這些討論我就不再贅述。這裏我們假定你必須使用某種循環。

 還要說明一點:我在此舉例說明有些情況下可能會出現死循環,而且盡可能的舉出最簡單的例子。在這些場景中,我並沒有說明循環的方法優於基於集合的解決方法,所以請不要對我所舉出的簡短例子作如此理解。

 使用SETSELECT都可能變量賦值失敗

 請讀者自行分析,下麵的例子就證明了這兩種情況:

DECLARE @i1 INT, @i2 INT, @i3 INT;

SELECT @i1=-1, @i2=-1, @i3=-1;

PRINT 'the following SELECT will not change the value of @i1'

PRINT 'because the result set is empty'

SELECT @i1=1 WHERE 1=2;

SELECT @i1 AS [@i1];

PRINT 'the following SET will change the value of @i2 to NULL'

PRINT 'because the result set is empty'

SET @i2=(SELECT 1 WHERE 1=2);

SELECT @i2 AS [@i2];

PRINT 'the following SELECT will not change the value of @i3'

PRINT 'because it will raise an exception'

SET @i3=(SELECT 1 UNION ALL SELECT 2);

SELECT @i3 AS [@i3];

由於返回空的數據集,下麵的SELECT語句將不會改變@i1的值。

@i1

-----------

-1

 

(1 row(s) affected)

 

由於返回空的數據集,下麵的SET語句將不會改變@i2的值。

@i2

-----------

NULL

 

(1 row(s) affected)

 

由於產生以異常(返回多行記錄),下麵的SELECT語句將不會改變@i3的值。

Msg 512, Level 16, State 1, Line 13

Subquery returned more than 1 value. This is not permitted when the subquery follows =, !=, <, <= , >, >= or when the subquery is used as an expression.

@i3

-----------

-1

 

(1 row(s) affected)

這是預期的結果,但你必須意識到這一點,否則你的循環可能無休止的執行下去。

Make sure you have read Tony Rogerson's post on the topic.

確保你已經閱讀了Tony Rogerson關於此主題的論文。

 SELECT語句改變變量值失敗時,可能導致死循環

請看下列表、樣例數據和存儲過程:

CREATE TABLE [data].[Orders](

      [OrderID] [int] NOT NULL,

      [OrderDate] [datetime] NOT NULL,

      IsProcessed CHAR(1) NOT NULL,

 CONSTRAINT [PK_Orders] PRIMARY KEY CLUSTERED

(

      [OrderID] ASC

)WITH (IGNORE_DUP_KEY = OFF)

)

GO

DELETE FROM [data].[Orders];

INSERT [data].[Orders](

      [OrderID],

      [OrderDate],

      IsProcessed)

SELECT 1, '20090420', 'N' UNION ALL

SELECT 2, '20090421', 'N' UNION ALL

SELECT 3, '20090422', 'N';

CREATE PROCEDURE dbo.ProcessNOrders

  @IDsIntervalSize INT

AS

DECLARE @minID INT, @ID INT;

SELECT @minID=MIN([OrderID]), @ID=MIN([OrderID]) FROM [data].[Orders];

WHILE @ID<(@minID+@IDsIntervalSize) BEGIN

  UPDATE [data].[Orders]

    SET IsProcessed='Y'

    WHERE [OrderID] = @ID;

  SELECT TOP 1 @ID=[OrderID]

    FROM [data].[Orders]

    WHERE IsProcessed='N'

    ORDER BY [OrderID];

  PRINT @ID;

END;

-- 本次調用成功

EXEC dbo.ProcessNOrders 2;

GO

 

-- 恢複數據

UPDATE [data].[Orders]

  SET IsProcessed='N';

GO

 

-- 本次調用處理了3orders數據後陷入死循環

-- 取消執行

EXEC dbo.ProcessNOrders 10;

解決辦法很簡單,如下所示——隻要確保賦值前變量值被改變

ALTER PROCEDURE dbo.ProcessNOrders

  @IDsIntervalSize INT

AS

DECLARE @minID INT, @ID INT;

SELECT @minID=MIN([OrderID]), @ID=MIN([OrderID]) FROM [data].[Orders];

WHILE @ID<(@minID+@IDsIntervalSize) BEGIN

  UPDATE [data].[Orders]

    SET IsProcessed='Y'

    WHERE [OrderID] = @ID;

  SET @ID = NULL;

  SELECT TOP 1 @ID=[OrderID]

    FROM [data].[Orders]

    WHERE IsProcessed='N'

    ORDER BY [OrderID];

  PRINT @ID;

END;

GO

UPDATE [data].[Orders]

  SET IsProcessed='N';

GO

 

-- 本次調用處理了3orders數據後完成處理

 

EXEC dbo.ProcessNOrders 10;

 

你也可以使用SET替換SELECT,那樣也可以解決這個問題:

 

ALTER PROCEDURE dbo.ProcessNOrders

  @IDsIntervalSize INT

AS

DECLARE @minID INT, @ID INT;

SELECT @minID=MIN([OrderID]), @ID=MIN([OrderID]) FROM [data].[Orders];

WHILE @ID<(@minID+@IDsIntervalSize) BEGIN

  UPDATE [data].[Orders]

    SET IsProcessed='Y'

    WHERE [OrderID] = @ID;

  SET @ID = (

        SELECT TOP 1 [OrderID]

            FROM [data].[Orders]

            WHERE IsProcessed='N'

            ORDER BY [OrderID]);

  PRINT @ID;

END;

 

當賦值產生異常而導致對變量值的改變失敗時

創建如下對象:

CREATE VIEW dbo.LongestWaitingNotProcessedOrder

AS

SELECT

      [OrderID],

      [OrderDate],

      IsProcessed

FROM [data].[Orders]

WHERE [OrderDate] = (SELECT MIN([OrderDate]) FROM [data].[Orders] WHERE IsProcessed='N')

AND IsProcessed='N'

GO

CREATE PROCEDURE dbo.ProcessOrder @OrderID SMALLINT

AS

SET NOCOUNT ON;

UPDATE [data].[Orders] SET IsProcessed='Y'

  WHERE [OrderID]=@OrderID;

RETURN @@ERROR;

GO

 

當未處理的order中沒有相同的order date時,下麵的循環會正確執行。

UPDATE [data].[Orders]

  SET IsProcessed='N';

 

DECLARE @ID INT;

SET @ID=-1;

WHILE @ID IS NOT NULL BEGIN

  SET @ID=(SELECT [OrderID]

    FROM dbo.LongestWaitingNotProcessedOrder);

  PRINT @ID;

  EXEC dbo.ProcessOrder @OrderID=@ID;

END

 

orders中存在兩條相同order date的未處理記錄時,對變量值的賦值過程(改變變量值)會失敗,從而導致死循環。運行這個腳本後回到上麵的循環語句,你會發現該循環變成了死循環。

 

UPDATE [data].[Orders]

  SET IsProcessed='N';

INSERT [data].[Orders](

      [OrderID],

      [OrderDate],

      IsProcessed)

SELECT 4, '20090421', 'N';

 

同樣,解決起來很簡單。你隻需把循環嵌入TRY…CATCH塊中,如下所示:

 

DECLARE @ID INT;

SET @ID=-1;

BEGIN TRY

      WHILE @ID IS NOT NULL BEGIN

        SET @ID=(SELECT [OrderID]

            FROM dbo.LongestWaitingNotProcessedOrder);

        PRINT @ID;

        EXEC dbo.ProcessOrder @OrderID=@ID;

      END

END TRY BEGIN CATCH

  SELECT ERROR_NUMBER(), ERROR_MESSAGE();

END CATCH

現在循環會在第一次出現異常後結束執行。然而,如果存儲過程處理一筆order失敗,你仍可能陷入死循環。

重新考慮基於集合的解決方法

你可以看出,在Transact SQL中開發健壯的循環是相當複雜的,其中確有一些技巧。你也許可以重新考慮使用基於集合的解決方式來替換循環方法。

使用表變量 

下麵的方法也相當健壯:

DECLARE @orderIDs TABLE(n INT, ID INT);

INSERT INTO @orderIDs(n, ID)

SELECT ROW_NUMBER() OVER(ORDER BY [OrderDate]), [OrderID]

  FROM [data].[Orders] WHERE IsProcessed='N';

DECLARE @ID INT, @n INT, @MaxN INT;

SELECT @n=1, @MaxN = MAX(n) FROM @orderIDs;

WHILE @n <= @MaxN BEGIN

  SELECT @ID = ID FROM @orderIDs WHERE n=@n;

  PRINT @ID;

  EXEC dbo.ProcessOrder @OrderID=@ID;

  SET @n=@n+1;

END

即使存儲過程處理一筆order時可能會失敗,循環仍然能夠完成,因為每筆order將隻被處理一次。

最後更新:2017-04-02 00:06:46

  上一篇:go Asp.Net頁麵請求性能大隱患 你是否做了這樣的事情
  下一篇:go FTP上傳文件示例