SQL Server:递归表更新

时间:2017-01-26 20:38:54

标签: sql-server sql-server-2008 triggers sql-server-2008-r2 recursive-query

我有一张桌子,让我们说一下TestTable。此表格包含以下列:

ID1 | ID2 | ID3 | LEVEL | PARENT_LEVEL | ENABLED | OBSOLET

所有列都是整数,ENABLED和OBSOLET只有两个可能的值(0或1)

LEVEL列可以有父级别,父级别可以是另一个父级别,依此类推,例如,想象下面的表格内容:

ID1 | ID2 | ID3 | LEVEL | PARENT_LEVEL | ENABLED | OBSOLET
 1     6     7      98       NULL          1         0
 1     6     6      99        98           1         0
 1     4     6     100        99           1         0
 1     2     3     200       100           1         0
 2     4     1     300       NULL          0         0
 3     3     4     400       NULL          0         1
 3     4     5     500       400           0         0

ID1,ID2和ID3是主键。

所以在树中代表这个:

 + 98
    |__ 99
         |__ 100
              |___ 200

 + 300

 + 400
    |__ 500

200有100作为父母,100有99作为父母,99有98作为父母。 300没有父母。 500有400作为父母,400有没有父母。

所以我需要的是一个更新查询,以递归方式更新字段' ENABLED',例如:

  • 如果我使用ENABLED = 1更新LEVEL 99,他的父母98必须更新为ENABLED = 1但不能更新为100和200.

  • 如果我使用ENABLED = 1更新LEVEL 200,他的父母100必须更新为ENABLED = 1,还有LEVEL 99和98,因为他们也有父母。

  • 如果我使用ENABLED = 1更新LEVEL 300,则只更新LEVEL 300,因为它没有父级。

所以我需要一个递归更新查询来更新字段ENABLED,直到LEVEL没有父节点(PARENT_LEVEL)。此外,我需要使用一个更新查询一次更新所有级别,而不仅仅执行更新的混凝土级别。

此外,在每次更新时,我需要检查字段' OBSOLET',如果LEVEL将字段OBSOLET设置为1,则意味着必须进行回滚,例如,在表格内容之上考虑,如果我将LEVEL 500更新为ENABLED = 1,没问题因为它的OBSOLET字段为0,所以它的字段ENABLED设置为1,然后通过递归,我们尝试将其父级LEVEL 400更新为ENABLED = 1,但是它的OBSOLET字段设置为1表示需要进行回滚,即LEVEL 400的ENABLED字段保持为0(未更新),设置为1的500级字段ENABLED也应恢复为0。

最后一个问题是此更新查询应该在此表TestTable的触发器中:

CREATE TRIGGER [dbo].[TG_TestTable]
ON  [dbo].[TestTable]  
FOR UPDATE
AS 
IF UPDATE ([ENABLED])
BEGIN

    // Update query must be here, so if field ENABLED is updated, trigger is fired again...so I don't know if disable trigger statement is necessary to be done before this update query and enable trigger after it.

END

这是因为要激活触发器,会对表TestTable的某些行执行更新,例如:

UPDATE [dbo].[TestTable] 
      SET ENABLED = 1
WHERE
      LEVEL IN (100,300,500); 

所以我试图在触发器中进行更新查询,但我不知道如何完成它:

UPDATE [dbo].[TestTable] 
      SET ENABLED= inserted.ENABLED
      ..... // SOMETHING ELSE
    FROM inserted 
    WHERE
        [dbo].[TestTable].ID1 = inserted.ID1
        AND
        [dbo].[TestTable].ID2 = inserted.ID2
        AND
        [dbo].[TestTable].ID3 = inserted.ID3
        AND 
        [dbo].[TestTable].PARENT_LEVEL = inserted.LEVEL;

那我怎么能做到这一点?也许使用递归函数或递归CTE?或者在时间执行和性能方面更好地在同一个表上进行递归触发?欢迎所有的想法。

1 个答案:

答案 0 :(得分:1)

我找到了解决方法。这是一个很长的问题,但它很容易理解 首先,创建并填充样本表(将此步骤保存在您未来的问题中)

CREATE TABLE TestTable
(
    ID1 int NOT NULL, 
    ID2 int NOT NULL, 
    ID3 int NOT NULL, 
    LEVEL int NOT NULL, 
    PARENT_LEVEL int, 
    ENABLED int, 
    OBSOLET int,
    PRIMARY KEY (ID1, ID2, ID3)
)

INSERT INTO TestTable VALUES
(1, 6, 7, 98,  NULL, 1, 0),
(1, 6, 6, 99,  98  , 0, 0),
(1, 4, 6, 100, 99  , 0, 0),
(1, 2, 3, 200, 100 , 0, 0),
(2, 4, 1, 300, NULL, 0, 0),
(3, 3, 4, 400, NULL, 0, 1),
(3, 4, 5, 500, 400 , 0, 0)

然后,创建INSTEAD OF UPDATE trigger,只会更新符合条件的记录 注意:这也会更新未更改启用值的记录,您很快就会在代码中看到它。

答案代码

CREATE TRIGGER tr_TestTable_IOU ON TestTable
INSTEAD OF UPDATE
AS

;WITH CTE AS
(   -- A recursive cte to get all the parents of the updated records
    SELECT  i.ID1,
            i.ID2,
            i.ID3,
            i.LEVEL,
            i.PARENT_LEVEL,
            i.ENABLED,
            i.OBSOLET
    FROM inserted i
    INNER JOIN deleted d ON i.ID1 = d.ID1
                        AND i.ID2 = d.ID2
                        AND i.ID3 = d.ID3
    WHERE i.ENABLED = 1
    AND d.ENABLED = 0
    -- The where clause will allow only records where enabled was changed from 0 to 1

    UNION ALL

    SELECT  t.ID1,
            t.ID2,
            t.ID3,
            t.LEVEL,
            t.PARENT_LEVEL,
            t.ENABLED,
            t.OBSOLET
    FROM TestTable t
    INNER JOIN CTE ON t.LEVEL = CTE.PARENT_LEVEL
), CTE_OBSOLET AS
(  -- A second recursive cte to get all the records where at least in one parent the value of OBSOLET = 1
    SELECT  i.ID1,
            i.ID2,
            i.ID3,
            i.LEVEL,
            i.PARENT_LEVEL,
            i.ENABLED,
            i.OBSOLET
    FROM TestTable i
    WHERE OBSOLET = 1

    UNION ALL

    SELECT  t.ID1,
            t.ID2,
            t.ID3,
            t.LEVEL,
            t.PARENT_LEVEL,
            t.ENABLED,
            1
    FROM TestTable t
    INNER JOIN CTE_OBSOLET ON t.PARENT_LEVEL = CTE_OBSOLET.LEVEL
)

    -- Update the enabled column to all relevant records (including parents)
    UPDATE t
    SET ENABLED = 1
    FROM TestTable t
    INNER JOIN CTE ON t.ID1 = CTE.ID1
                  AND t.ID2 = CTE.ID2
                  AND t.ID3 = CTE.ID3
    LEFT JOIN CTE_OBSOLET ON t.ID1 = CTE_OBSOLET.ID1
                         AND t.ID2 = CTE_OBSOLET.ID2
                         AND t.ID3 = CTE_OBSOLET.ID3
    WHERE CTE_OBSOLET.LEVEL IS NULL -- Assuming the LEVEL is not nullable. Any other not nullable column can be used here


    -- Update records where columns other then ENABLED was changed. 
    -- Since this is an instead of update trigger, you have to include this to enable updates on other columns.
    -- This assumes that you can't update the columns of the primary key (ID1, ID2 and ID3).
    UPDATE t
    SET LEVEL = i.LEVEL,
        PARENT_LEVEL = i.PARENT_LEVEL,
        OBSOLET = i.OBSOLET 
    FROM TestTable t
    INNER JOIN inserted i ON t.ID1 = i.ID1
                          AND t.ID2 = i.ID2
                          AND t.ID3 = i.ID3
    INNER JOIN deleted d ON i.ID1 = d.ID1
                         AND i.ID2 = d.ID2
                         AND i.ID3 = d.ID3
    WHERE i.LEVEL <> d.LEVEL
    OR d.PARENT_LEVEL <> i.PARENT_LEVEL
    OR d.OBSOLET <> i.OBSOLET
GO

测试:

SELECT *
FROM TestTable

结果:

ID1     ID2     ID3     LEVEL   PARENT_LEVEL    ENABLED     OBSOLET
1       6       7       98      NULL            1           0
1       6       6       99      98              0           0
1       4       6       100     99              0           0
1       2       3       200     100             0           0
2       4       1       300     NULL            0           0
3       3       4       400     NULL            0           1
3       4       5       500     400             0           0

做一些更新:

UPDATE TestTable
SET ENABLED = 1
WHERE LEVEL IN(200, 500)

UPDATE TestTable
SET ENABLED = 1,
    OBSOLET = 1
WHERE LEVEL = 500

测试结果:

SELECT *
FROM TestTable

结果:

ID1     ID2     ID3     LEVEL   PARENT_LEVEL    ENABLED     OBSOLET
1       6       7       98      NULL            1           0
1       6       6       99      98              1           0
1       4       6       100     99              1           0
1       2       3       200     100             1           0
2       4       1       300     NULL            0           0
3       3       4       400     NULL            0           1
3       4       5       500     400             0           1