【问题标题】:SQL Server: Recursive table updateSQL Server:递归表更新
【发布时间】:2017-01-26 20:38:53
【问题描述】:

我有一张桌子,比如说 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',例如:

  • 如果我将 LEVEL 99 更新为 ENABLED=1,他的父母也必须将 98 更新为 ENABLED=1,但不是 LEVEL 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(未更新),而 level 500 的设置为 1 的字段 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?还是在时间执行和性能方面更好地在同一张表上使用递归触发器?欢迎所有想法。

【问题讨论】:

  • 您真正需要注意的一件事是,UPDATE 并不意味着值已更改。这意味着它是更新语句中的一列。我怀疑如果值实际发生变化,您只会实际更新父母或孩子。为此,您必须将表与插入的表进行比较,以查看值是否不同。
  • 对于手头的实际任务,似乎递归 cte(可能一个为 up 而另一个为 down)将是这里的解决方案。
  • @SeanLange 你能发布一个关于递归 CTE 的小例子吗?谢谢!
  • 您可以尝试搜索。仅在 SO 上就有数千个示例,在任何搜索引擎上都有数十万个示例。

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


【解决方案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

【讨论】:

  • @user1624552 你测试了这个答案吗?
猜你喜欢
  • 2013-07-06
  • 2016-03-28
  • 1970-01-01
  • 1970-01-01
  • 2013-08-03
  • 1970-01-01
  • 1970-01-01
  • 2021-12-12
  • 1970-01-01
相关资源
最近更新 更多