复制树状结构的一个分支 table

Copying a branch of tree-like structured table

我有以下 table,其中 ID 是 table 的 pk 并且是 IDENTITY

+----+----------+-----------+-------------+
| ID | ParentID | SomeValue |  FullPath   |
+----+----------+-----------+-------------+
|  1 | NULL     | A         | (1)         |
|  2 | 1        | A.1       | (1)/(2)     |
|  3 | 2        | A.1.1     | (1)/(2)/(3) |
|  4 | NULL     | B         | (4)         |
|  5 | 4        | B.1       | (4)/(5)     |
|  6 | 4        | B.2       | (4)/(6)     |
|  7 | 6        | B.2.1     | (4)/(6)/(7) |
+----+----------+-----------+-------------+

这个table表示存储在hierarchical way中的数据。我正在创建一个将 IDnew_ParentID 作为参数作为输入的过程; ID(及其子项和子项的子项等)将是要复制到 new_ParentID.

的分支

我启动了程序,但我不知道如何获取我创建的父项的新 ID 以便添加它的子项。例如,如果我想将A.1(和A.1.1)复制到B.2中,一旦A.1-Copied被创建,我不知道它的ID把它写成ParentID 的 A.1.1-已复制。我知道函数 SCOPE_IDENTITY,但我不知道如何在 CTE 中使用它。这是我目前拥有的:

;WITH Branch
AS
(
    SELECT  ID,
            ParentGroupID,
            SomeValue
    FROM    
        #Table1 A
    WHERE
        ID = @ID
    UNION ALL
    SELECT  E.ID,
            E.ParentGroupID,
            E.SomeValue
    FROM
        #Table1 E
    INNER JOIN Branch T
            ON  T.ID = E.ParentGroupID
) 
INSERT INTO #Table1
SELECT
    CASE WHEN ParentGroupID IS NULL 
        THEN @new_ParentID
        ELSE ???,
    SomeValue + '-Copied'
FROM    
    Branch

如何设法使用 SCOPE_IDENTITY 正确设置我复制的分支的子项的新父项?

编辑:

假设我想复制 ID 为 4 的分支(所以整个 B 分支)到 ID 2(所以 A.1 分支),我们应该有如下数据:

+----+----------+------------+-----------------------+
| ID | ParentID | SomeValue  |       FullPath        |
+----+----------+------------+-----------------------+
|  1 | NULL     | A          | (1)                   |
|  2 | 1        | A.1        | (1)/(2)               |
|  3 | 2        | A.1.1      | (1)/(2)/(3)           |
|  4 | NULL     | B          | (4)                   |
|  5 | 4        | B.1        | (4)/(5)               |
|  6 | 4        | B.2        | (4)/(6)               |
|  7 | 6        | B.2.1      | (4)/(6)/(7)           |
|  8 | 2        | B-Copy     | (1)/(2)/(8)           |
|  9 | 8        | B.1-Copy   | (1)/(2)/(8)/(9)       |
| 10 | 8        | B.2-Copy   | (1)/(2)/(8)/(10)      |
| 11 | 10       | B.2.1-Copy | (1)/(2)/(8)/(10)/(11) |
+----+----------+------------+-----------------------+

我有程序可以在之后更新 SomeValueFullPath 值,所以不用担心这些!我对如何重现层次结构感兴趣

下面是插入示例数据的代码:

CREATE TABLE #Data
(
    ID INT IDENTITY(1,1),
    ParentID INT,
    SomeValue VARCHAR(30),
    FullPath VARCHAR(255)
)

INSERT INTO #Data VALUES(NULL,'A','(1)')
INSERT INTO #Data VALUES('1','A.1','(1)/(2)')
INSERT INTO #Data VALUES('2','A.1.1','(1)/(2)/(3)')
INSERT INTO #Data VALUES(NULL,'B','(4)')
INSERT INTO #Data VALUES('4','B.1','(4)/(5)')
INSERT INTO #Data VALUES('4','B.2','(4)/(6)')
INSERT INTO #Data VALUES('6','B.2.1','(4)/(6)/(7)')

好的,我们不要拐弯抹角,这很乱,需要扫几下。

我们需要先在这里使用一个MERGE(没有UPDATE子句),这样我们就可以OUTPUT新旧ID值到一个table变量中.然后,之后我们需要使用 UPDATE 来更新新路径的所有路径。

您可能 UPDATE MERGE 中的先前级别,同时 INSERT MERGE, 中的当前级别 但是,我没有去沿着这条路走下去,因为它可能更混乱。因此,在插入行之后,我使用进一步的 rCTe 来创建新路径和 UPDATE 它们。

这给你下面(注释)SQL:

USE Sandbox;
GO

CREATE TABLE dbo.Data
(
    ID INT IDENTITY(1,1),
    ParentID INT,
    SomeValue VARCHAR(30),
    FullPath VARCHAR(255)
)

INSERT INTO dbo.Data
--VALUES has supported multiple rows in 2008, you should be making use of it.
VALUES(NULL,'A','(1)')
     ,('1','A.1','(1)/(2)')
     ,('2','A.1.1','(1)/(2)/(3)')
     ,(NULL,'B','(4)')
     ,('4','B.1','(4)/(5)')
     ,('4','B.2','(4)/(6)')
     ,('6','B.2.1','(4)/(6)/(7)')
GO
--There are your parameters
DECLARE @BranchToCopy int,
        @CopysParent int;

SET @BranchToCopy = 4;
SET @CopysParent = 2;

--Table which will have the data to INSERT in
DECLARE @NewData table (ID int,
                        ParentID int,
                        SomeValue varchar(30),
                        FullPath varchar(255),
                        Level int);

--Will be used in the MERGE's OUTPUT clause to link the new and old IDs
DECLARE @Keys table (OldID int,
                     NewID int,
                     Level int);

--Get the hierachical data and INSERT into the @NewData variable
WITH rCTE AS(
    SELECT D.ID,
           D.ParentID,
           D.SomeValue,
           D.FullPath,
           1 AS Level
    FROM dbo.Data D
    WHERE ID = @BranchToCopy
    UNION ALL
    SELECT D.ID,
           D.ParentID,
           D.SomeValue,
           D.FullPath,
           r.[Level] + 1
    FROM dbo.Data D
         JOIN rCTE r ON D.ParentID = r.ID)
INSERT INTO @NewData (ID,ParentID,SomeValue,FullPath,Level)
SELECT r.ID,
       r.ParentID,
       CONCAT(r.SomeValue,'-Copy'),
       r.FullPath,
       r.[Level]
FROM rCTE r;

--Uncomment to see results
--SELECT *
--FROM @NewData;

--Yes, we're using a WHILE!
--This, however, is what is known as a "set based loop"
DECLARE @i int = 1;
WHILE @i <= (SELECT MAX(Level) FROM @NewData) BEGIN

    --We use MERGE here as it allows us to OUTPUT columns that weren't inserted into the table
    MERGE INTO dbo.Data USING (SELECT ND.ID,
                                      CASE ND.ID WHEN @BranchToCopy THEN @CopysParent ELSE K.NewID END AS Parent,
                                      ND.SomeValue,
                                      ND.Level
                               FROM @NewData ND
                                    LEFT JOIN @Keys K ON ND.ParentID = K.OldID
                               WHERE ND.Level = @i) U ON 0=1
    WHEN NOT MATCHED THEN
        INSERT (ParentID, SomeValue)
        VALUES (U.Parent, U.SomeValue)
        OUTPUT U.ID, inserted.ID, U.Level
        INTO @Keys (OldID, NewID, Level);

    --Increment
    SET @i = @i + 1;
END;

--Uncomment to see results
--SELECT *
--FROM dbo.[Data];

--Now we need to do the FullPath, as that would be a pain to do on the fly
DECLARE @Paths table (ID int, NewPath varchar(255));

--Work out the new paths
WITH rCTE AS(
    SELECT D.ID,
           D.ParentID,
           D.SomeValue,
           D.FullPath,
           CONVERT(varchar(255),NULL) AS NewPath
    FROM dbo.Data D
    WHERE D.ID = @CopysParent
    UNION ALL
    SELECT D.ID,
           D.ParentID,
           D.SomeValue,
           D.FullPath,
           CONVERT(varchar(255),CONCAT(ISNULL(r.FullPath,r.NewPath),'/(',D.ID,')'))
    FROM dbo.Data D
         JOIN rCTE r ON D.ParentID = r.ID
         JOIN @Keys K ON D.ID = K.NewID) --As we want only the new rows
INSERT INTO @Paths (ID, NewPath)
SELECT ID, NewPath
FROM rCTe
WHERE FullPath IS NULL;
--Update the table
UPDATE D
SET FullPath = P.NewPath
FROM dbo.Data D
     JOIN @Paths P ON D.ID = P.ID;

SELECT *
FROM dbo.Data;

GO
--Clean up
DROP TABLE dbo.Data;

DB<>Fiddle

这是一个仅使用 CTE 的解决方案: 路径 config as ( ... 定义了用于计算的 from 和 to id。这一切都可以在 TVF 中完成。

WITH T AS (
select 1 id, null parentid, 'A'     somevalue, '(1)'         fullpath union all
select 2 id, 1    parentid, 'A.1'   somevalue, '(1)/(2)'     fullpath union all
select 3 id, 2    parentid, 'A.1.1' somevalue, '(1)/(2)/(3)' fullpath union all
select 4 id, NULL parentid, 'B'     somevalue, '(4)'         fullpath union all
select 5 id, 4    parentid, 'B.1'   somevalue, '(4)/(5)'     fullpath union all
select 6 id, 4    parentid, 'B.2'   somevalue, '(4)/(6)'     fullpath union all
select 7 id, 6    parentid, 'B.2.1' somevalue, '(4)/(6)/(7)' fullpath
)

, config as (
select 4 from_id, 2 to_id
)

, maxid as (
select max(id) maxid from t
)

, initpath as (
select fullpath from t cross join config where id = to_id
)

, subset_from as (
select t.*, maxid + ROW_NUMBER() over (order by id) new_id, ROW_NUMBER() over (order by id) rn from t cross join config cross join maxid where fullpath like '(' + cast(from_id as varchar) + ')%'
)

, subset_count as (
select count(*) subset_count from subset_from
)

, fullpath_replacements (id, parentid, somevalue, new_id, fullpath, new_fullpath, lvl) as (
select id, parentid, somevalue, new_id, fullpath, replace(fullpath, '(' + cast((select sf.id from subset_from sf where rn = 1) as varchar) + ')', '(' + cast((select sf.new_id from subset_from sf where rn = 1) as varchar) + ')'), 1 
from subset_from

union all
select id, parentid, somevalue, new_id, fullpath, replace(new_fullpath, '(' + cast((select sf.id from subset_from sf where sf.rn = fr.lvl + 1) as varchar) + ')', '(' + cast((select sf.new_id from subset_from sf where sf.rn = fr.lvl + 1) as varchar) + ')'), fr.lvl + 1 
from fullpath_replacements fr where fr.lvl < (select subset_count from subset_count)
)

, final_replacement as (
select id, parentid, somevalue, new_id, fullpath, (select fullpath from t where t.id = (select to_id from config)) + '/' + new_fullpath new_fullpath, isnull((select sf.new_id from subset_from sf where sf.id = fr.parentid), (select to_id from config)) new_parentid
  from fullpath_replacements fr where fr.lvl = (select subset_count from subset_count)
)

select id, parentid, somevalue, fullpath 
from (
    select * from t
    union all
    select new_id, new_parentid, somevalue, new_fullpath from final_replacement
) t order by id

想法是使用 row_number window 函数创建新的 ID(请参阅 subset_from 部分)。 然后通过 id 在完整路径 id 中进行替换。这是使用递归 CTE fullpath_replacements 来模拟循环来完成的。

这是可行的,因为在完整路径中我总是可以使用括号来标识需要交换完整路径的哪一部分。

这是输出: