合并两个父>子表集

Posted

技术标签:

【中文标题】合并两个父>子表集【英文标题】:Merging two parent > child table sets 【发布时间】:2015-05-13 13:28:56 【问题描述】:

我需要将两个父 > 子表集中的数据合并/组合到第三个父 > 子表中。

表格如下所示:

这三组表的唯一区别是TableC 有一个TableType 列来帮助辨别TableA 记录和TableB 记录之间的区别。

我的第一个想法是使用游标。这是创建表结构的代码,插入一些记录,然后将数据合并在一起。效果很好,太好了....

--Create the tables

CREATE TABLE TableA
(
    ID int not null identity primary key,
    Name VARCHAR(30)
);

CREATE TABLE TableAChild
(
    ID int not null identity primary key,
    Parent int not null,
    Name VARCHAR(30),
    CONSTRAINT FK_A FOREIGN KEY (Parent) REFERENCES TableA(ID)
);

CREATE TABLE TableB
(
    ID int not null identity primary key,
    Name VARCHAR(30)
);

CREATE TABLE TableBChild
(
    ID int not null identity primary key,
    Parent int not null,
    Name VARCHAR(30),
    CONSTRAINT FK_B FOREIGN KEY (Parent) REFERENCES TableB(ID)
);

CREATE TABLE TableC
(
    ID int not null identity primary key,
    TableType VARCHAR(1),
    Name VARCHAR(30)
);

CREATE TABLE TableCChild
(
    ID int not null identity primary key,
    Parent int not null,
    Name VARCHAR(30),
    CONSTRAINT FK_C FOREIGN KEY (Parent) REFERENCES TableC(ID)
);

-- Insert some test records.. 

INSERT INTO TableA (Name) Values ('A1')
INSERT INTO TableAChild (Name, Parent) VALUES ('A1Child', SCOPE_IDENTITY())
INSERT INTO TableB (Name) Values ('B1')
INSERT INTO TableBChild (Name, Parent) VALUES ('B1Child', SCOPE_IDENTITY())

-- Needed throughout.. 
DECLARE @ID INT

-- Merge TableA and TableAChild into TableC and TableCChild
DECLARE TableACursor CURSOR
    -- Get the primary key from TableA
    FOR SELECT ID FROM TableA
OPEN TableACursor
    FETCH NEXT FROM TableACursor INTO @ID

    WHILE @@FETCH_STATUS = 0
    BEGIN
        -- INSERT INTO SELECT the parent record into TableC, being sure to specify a TableType
        INSERT INTO TableC (Name, TableType) SELECT Name, 'A' FROM TableA WHERE ID = @ID

        -- INSERT INTO SELECT the child record into TableCChild using the parent ID of the last row inserted (SCOPE_IDENTITY())
        -- and the current record from the cursor (@ID).
        INSERT INTO TableCChild(Name, Parent) SELECT Name, SCOPE_IDENTITY() FROM TableAChild WHERE Parent = @ID

        FETCH NEXT FROM TableACursor INTO @ID
    END;

CLOSE TableACursor
DEALLOCATE TableACursor

-- Repeat for TableB
DECLARE TableBCursor CURSOR
    FOR SELECT ID FROM TableB
OPEN TableBCursor
    FETCH NEXT FROM TableBCursor INTO @ID

    WHILE @@FETCH_STATUS = 0
    BEGIN
        INSERT INTO TableC (Name, TableType) SELECT Name, 'B' FROM TableB WHERE ID = @ID
        INSERT INTO TableCChild(Name, Parent) SELECT Name, SCOPE_IDENTITY() FROM TableBChild WHERE Parent = @ID
        FETCH NEXT FROM TableBCursor INTO @ID
    END;

CLOSE TableBCursor
DEALLOCATE TableBCursor

现在,我的问题:

我一直被告知游标不好。但我找不到另一种方法。我想知道是否有办法用 CTE 做到这一点? 如果光标在这种情况下是合适的,我是怎么做的?有没有更好的方法来做我所做的事情?它对我来说看起来不是很干燥,但我不是 SQL 专家。

最后,如果您想重新运行上面的查询,这里有一个小脚本来删除已创建的表。

DROP TABLE TableAChild
DROP TABLE TableBChild
DROP TABLE TableCChild

DROP TABLE TableA
DROP TABLE TableB
DROP TABLE TableC

正确的结果应该是这样的:

【问题讨论】:

如果tableatableb 中的名称字段在每个表中都是唯一的,那么您可以在没有游标的情况下重写它,而只需使用join。但是,如果它们不是唯一的,那么光标是我看到的从父级获取 id 字段的唯一选项。 @sgeddes 您指的是“名称”列中的吗?如果是这样,它们就不是唯一的。 为什么不在 TableC 中为 LegacyID 添加另一列。这将为您提供使用联接的值。这种RBAR没必要做。 @SeanLange Hrmmm,你让我思考,但我不确定它是如何工作的。我的数据库中的 TableX.ID 列和 TableXChild.Parent 列之间存在关系(很抱歉未将其包含在示例中,因为我认为没有必要)。而且我绝对不想留下一个垃圾 LegacyID 列。你能解释一下吗?可能在答案中? 您可以将该 LegacyID 列添加为临时内容。它将允许使用它将子行加入新的父行。完成数据合并后,您可以删除该列,因为它不再具有任何意义。 【参考方案1】:

您可以按照 Adam Machanic 在Dr. OUTPUT or: How I Learned to Stop Worrying and Love the MERGE 和this 问题中的描述使用merge 来获取表变量中新标识值和旧主键值之间的映射以及插入时的使用到您的子表。

declare @T table(ID int, IDC int);

merge dbo.TableC as C
using dbo.TableA as A
on 0 = 1
when not matched by target then
  insert (TableType, Name) values('A', A.Name)
output A.ID, inserted.ID into @T(ID, IDC);

insert into dbo.TableCChild(Parent, Name)
select T.IDC, AC.Name
from dbo.TableAChild as AC
  inner join @T as T
    on AC.Parent = T.ID;

delete from @T;

merge dbo.TableC as C
using dbo.TableB as B
on 0 = 1
when not matched by target then
  insert (TableType, Name) values('B', B.Name)
output B.ID, inserted.ID into @T(ID, IDC);

insert into dbo.TableCChild(Parent, Name)
select T.IDC, BC.Name
from dbo.TableBChild as BC
  inner join @T as T
    on BC.Parent = T.ID;

SQL Fiddle

【讨论】:

这绝对是非常神奇的。而且,令人惊讶的是,我什至没有看过你引用的文章就明白了。 0=1 感觉有点骇人听闻。似乎您正在使用merge,以便您可以利用outputinserted。在 .NET 中,我们倾向于对这种滥用行为不屑一顾。但是,据我所知,merge 的在线用法似乎是可以接受的。让我们拭目以待,看看还有没有其他的投稿.. 非常巧妙地使用了合并,因为正常插入的输出无法将 A.id 放入表变量中,这就是为什么在我的解决方案中我必须跳过循环以匹配两个 id【参考方案2】:

这是一种无需光标或其他 RBAR 类型的东西的方法。

ALTER TABLE TableC ADD LegacyID INT
GO

INSERT INTO TableC (TableType, Name, LegacyID)
SELECT 'A', Name, ID
FROM TableA

INSERT TableCChild
SELECT C.ID, AC.Name
FROM TableAChild AC
JOIN TableA A ON A.Id = AC.ID
JOIN TableC C ON C.LegacyID = A.ID AND C.TableType = 'A'

INSERT INTO TableC (TableType, Name, LegacyID)
SELECT 'B', Name, ID
FROM TableB

INSERT TableCChild
SELECT C.ID, AC.Name
FROM TableBChild AC
JOIN TableB A ON A.Id = AC.ID
JOIN TableC C ON C.LegacyID = A.ID AND C.TableType = 'B'

ALTER TABLE TableC DROP COLUMN LegacyID
GO

【讨论】:

很遗憾,这是一个非常不完整的例子。另外,TableX.ID 是一个标识列,因此您无法将记录插入其中。并且您无法打开标识插入,因为在合并来自 TableB 和 TableBChild 的记录时会发生冲突。不过我觉得你有一些事情要做,所以我很乐意看到它完成。 您甚至没有使用 LegacyId。 :) 我想他的意思是from TableAChild ac join TableC A on A.LegacyID = ac.ID 糟糕。我完全搞砸了。请查看现在使用 TableC 检索新标识值的更新。 无法正常运行。您可以使用我包含在 OP 中的脚本来生成表格和数据以确认您的答案正常吗?【参考方案3】:

您可以使用map 表根据某个键将新旧 id 链接在一起。

在我的示例中,我使用插入到TableC 的顺序。

    创建带有标识列的映射表。 根据IDTableA 的顺序在TableC 表中添加数据并获取地图中插入的ID 使用与TableA.id相同的顺序得到ROWNUMBER()并将其与映射表的标识列匹配,并更新映射中的old_id以匹配TableA.idTableC.id。 使用映射插入到TableCChild 表中 截断地图并冲洗并重复其他表格。

示例查询

CREATE TABLE  #map(id int identity,new_id int,old_id int);
INSERT INTO TableC
(
    TableType,
    Name
)output inserted.id into #map(new_id)
SELECT 'A',Name
FROM TableA
ORDER BY ID


update m
set m.old_id = ta.id
FROM #map m
inner join 
(
select row_number()OVER(order by id asc) rn,id
from tableA
)ta on ta.rn = m.id

INSERT INTO TableCChild (Name, Parent) 
SELECT Name,M.new_ID
FROM #Map M
INNER JOIN TableAChild TA ON M.old_id = TA.Parent

TRUNCATE TABLE #map

INSERT INTO TableC
(
    TableType,
    Name
)output inserted.id into #map(new_id)
SELECT 'B',Name
FROM TableB
ORDER BY ID

update m
set m.old_id = tb.id
FROM #map m
inner join 
(
select row_number()OVER(order by id asc) rn,id
from tableB
)tb on tb.rn = m.id

INSERT INTO TableCChild (Name, Parent) 
SELECT Name,M.new_ID
FROM #Map M
INNER JOIN TableBChild TB ON M.old_id = TB.Parent

DROP TABLE #Map

【讨论】:

如果可以在表TableC 中添加额外的列,您可以像 Sean Lange 建议的那样将表本身用作地图,然后删除该列。 这个答案产生了预期的结果。它看起来比游标方法更复杂。不过我得研究一下。让我们通过投票来看看其他人的想法。【参考方案4】:

如果Name在TableA中是唯一的并且在TableB中是唯一的,我只是写了下面的SQL来做到这一点

INSERT INTO TableCChild
  (
    Parent,
    NAME
  )
SELECT tc.ID,
       ta.Name
FROM   TableAChild  AS ta
       JOIN TableA a
            ON  a.ID = ta.Parent
       JOIN TableC  AS tc
            ON  tc.Name = a.Name
                AND tc.TableType = 'A' 
UNION
SELECT tc.ID,
       tb.Name
FROM   TableBChild  AS tb
       JOIN TableB b
            ON  b.ID = tb.Parent
       JOIN TableC  AS tc
            ON  tc.Name = b.Name
                AND tc.TableType = 'B' 

如果名称不是唯一的并且只有 ID 是唯一标识符,那么我将按照建议添加 LegacyId,然后代码如下

/* Change Table C to Have LegacyId as well and this is used to find the New Key for Inserts
CREATE TABLE TableC
(
    ID            INT NOT NULL IDENTITY PRIMARY KEY,
    TableType     VARCHAR(1),
    LegacyId     INT,
    NAME          VARCHAR(30)
);
*/

INSERT INTO TableC (Name, TableType, LegacyId) 
SELECT DISTINCT NAME,
       'A', 
       Id
FROM   TableA
UNION
SELECT DISTINCT NAME,
       'B',
       Id
FROM   TableB

    INSERT INTO TableCChild
      (
        Parent,
        NAME
      )
    SELECT tc.ID,
           ta.Name
    FROM   TableAChild  AS ta
           JOIN TableA a
                ON  a.ID = ta.Parent
           JOIN TableC  AS tc
                ON  tc.LegacyId = a.Id
                    AND tc.TableType = 'A' 
    UNION
    SELECT tc.ID,
           tb.Name
    FROM   TableBChild  AS tb
           JOIN TableB b
                ON  b.ID = tb.Parent
           JOIN TableC  AS tc
                ON  tc.LegacyId = b.Id
                    AND tc.TableType = 'B' 

【讨论】:

这和肖恩的回答不一样吗,除了你把所有东西都联合起来吗?如果是这样,UNION 在这里有什么好处? 几乎和他的一样,实际上联合并没有真正的优势,但是在第一个查询中,如果你的名字在 a 中是唯一的并且在 b 中是唯一的,你不需要遗留 ID ,意味着更简单的解决方案。我测试了我的,并确保它运行时也没有错误:) 另外,如果有必要,我更支持保留 legacyid,如果没有,则将其全部关闭。【参考方案5】:

我们可以通过关闭标识列来实现这一点,直到我们完成插入,如下例所示。

--Create the tables

CREATE TABLE TableA
(
    ID int not null identity primary key,
    Name VARCHAR(30)
);

CREATE TABLE TableAChild
(
    ID int not null identity primary key,
    Parent int not null,
    Name VARCHAR(30),
    CONSTRAINT FK_A FOREIGN KEY (Parent) REFERENCES TableA(ID)
);

CREATE TABLE TableB
(
    ID int not null identity primary key,
    Name VARCHAR(30)
);

CREATE TABLE TableBChild
(
    ID int not null identity primary key,
    Parent int not null,
    Name VARCHAR(30),
    CONSTRAINT FK_B FOREIGN KEY (Parent) REFERENCES TableB(ID)
);

CREATE TABLE TableC
(
    ID int not null identity primary key,
    TableType VARCHAR(1),
    Name VARCHAR(30)
);

CREATE TABLE TableCChild
(
    ID int not null identity primary key,
    Parent int not null,
    Name VARCHAR(30),
    CONSTRAINT FK_C FOREIGN KEY (Parent) REFERENCES TableC(ID)
);

-- Insert some test records.. 

INSERT INTO TableA (Name) Values ('A1')
INSERT INTO TableAChild (Name, Parent) VALUES ('A1Child', SCOPE_IDENTITY())
INSERT INTO TableB (Name) Values ('B1')
INSERT INTO TableBChild (Name, Parent) VALUES ('B1Child', SCOPE_IDENTITY())

SET IDENTITY_INSERT TableC ON
INSERT INTO TableC(ID, TableType, Name)
SELECT ID, 'A', Name FROM TableA

INSERT INTO TableCChild(Parent, Name)
SELECT Parent, Name FROM TableAChild

DECLARE @MAXID INT
SELECT @MAXID = MAX(ID) FROM TableC
PRINT @MAXID

SET IDENTITY_INSERT TableC ON
INSERT INTO TableC(ID, TableType, Name)
SELECT ID + @MAXID, 'B', Name FROM TableB
SET IDENTITY_INSERT TableC OFF

INSERT INTO TableCChild(Parent, Name)
SELECT Parent + @MAXID, Name FROM TableBChild

SET IDENTITY_INSERT TableC OFF

SELECT * FROM TableC
SELECT * FROM TableCChild

DROP TABLE TableAChild
DROP TABLE TableBChild
DROP TABLE TableCChild

DROP TABLE TableA
DROP TABLE TableB
DROP TABLE TableC

【讨论】:

【参考方案6】:

如果您需要在第三个表 TableC 和 TableCChild 中插入记录以供以后使用,那么可以在这些表中插入数据,但如果您暂时只需要此表数据在存储过程中使用它,那么您也可以只需使用前两个表即可获得所需的结果。

select * from (
select a.ID,'A' as TableType,a.Name from TableA a inner join TableAChild b on a.ID=b.ID
union
select a.ID,'B' as TableType,a.Name  from TableB a inner join TableBChild b on a.ID=b.ID) TableC

类似获取 TableCChild

select * from 
(
select b.ID,b.Parent,b.Name  from TableA a inner join TableAChild b on a.ID=b.ID
union
select b.ID,b.Parent,b.Name   from TableB a inner join TableBChild b on a.ID=b.ID) TableCChild

如果你必须在 TableC 和 TableCChild 中插入,那么你必须使用 ID 和 TableType 的主键重新创建 TableC,并关闭 ID 列的标识。

【讨论】:

以上是关于合并两个父>子表集的主要内容,如果未能解决你的问题,请参考以下文章

Redshift 不使用交错排序键执行合并连接

两个子表的 MySQL JOIN 问题

在 NDB 中按父字段查询子表和排序结果

SQL 如何把两个表相关联的数据一同删除

必须知道的八大种排序算法java实现 归并排序算法堆排序算法详解

在集群模式下添加子表的行时 H2 参照完整性违规