将 Table 拆分为多对多关系:数据迁移
Split Table into many to many relationship: Data Migration
我想知道在将 Table 拆分为多对多关系时如何最好地迁移我的数据。我已经做了一个简化的例子,我也会 post 我想出的一些解决方案。
我正在使用 Postgresql 数据库。
迁移前
Table 人
ID Name Pet PetName
1 Follett Cat Garfield
2 Rowling Hamster Furry
3 Martin Cat Tom
4 Cage Cat Tom
迁移后
Table 人
ID Name
1 Follett
2 Rowling
3 Martin
4 Cage
Table宠物
ID Pet PetName
6 Cat Garfield
7 Hamster Furry
8 Cat Tom
9 Cat Tom
Table 人宠物
FK_Person FK_Pet
1 6
2 7
3 8
4 9
备注:
- 我将专门复制 Pet Table 中的条目(因为在我的情况下 - 由于其他相关数据 - 其中一个可能仍被客户编辑 table 而另一个可能不是).
- 没有唯一标识 "Pet" 记录的列。
- 对我来说,在 PersonPet table 或 3-9 和 4-8 中链接 3-8 和 4-9 并不重要。
- 我还省略了所有处理 table 模式更改的代码,因为据我所知,这与这个问题无关。
我的解决方案
- 创建宠物时 Table 临时添加一列,其中包含用于创建此条目的人物 Table 的 ID。
ALTER TABLE Pet ADD COLUMN IdPerson INTEGER;
INSERT INTO Pet (Pet, PetName, IdPerson)
SELECT Pet, PetName, ID
FROM Person;
INSERT INTO PersonPet (FK_Person, FK_Pet)
SELECT ID, IdPerson
FROM Pet;
ALTER TABLE Pet DROP Column IdPerson;
- 避免临时修改宠物table
INSERT INTO Pet (Pet, PetName)
SELECT Pet, PetName
FROM Person;
WITH
CTE_Person
AS
(SELECT
Id, Pet, PetName
,ROW_NUMBER() OVER (PARTITION BY Pet, PetName ORDER BY Id) AS row_number
FROM Person
)
,CTE_Pet
AS
(SELECT
Id, Pet, PetName
,ROW_NUMBER() OVER (PARTITION BY Pet, PetName ORDER BY Id) AS row_number
FROM Pet
)
,CTE_Joined
AS
(SELECT
CTE_Person.Id AS Person_Id,
CTE_Pet.Id AS Pet_Id
FROM
CTE_Person
INNER JOIN CTE_Pet ON
CTE_Person.Pet = CTE_Pet.Pet
CTE_Person.PetName = CTE_Pet.PetName
AND CTE_Person.row_number = CTE_Pet.row_number
)
INSERT INTO PersonPet (FK_Person, FK_Pet)
SELECT Person_Id, Pet_Id from CTE_Joined;
问题
- 两种解法都正确吗? (我已经测试了第二种解决方案,结果似乎是正确的,但我可能遗漏了一些极端情况)
- 这两个解的advantages/disadvantages是什么?
- 是否有更简单的方法来执行相同的数据迁移? (出于好奇,我也会对稍微修改我的约束的答案感兴趣(例如,Pet table 中没有重复的条目),但请指出哪些:))。
是的,您的两个解决方案都是正确的。他们让我想起了 .
几个笔记。
在 Pet
table 中添加额外列 PersonID
的第一个变体可以使用 RETURNING
子句在单个查询中完成。
-- Add temporary PersonID column to Pet
WITH
CTE_Pets
AS
(
INSERT INTO Pet (PersonID, Pet, PetName)
SELECT Person.ID, Person.Pet, Person.PetName
FROM Person
RETURNING ID AS PetID, PersonID
)
INSERT INTO PersonPet (FK_Person, FK_Pet)
SELECT PersonID, PetID
FROM CTE_Pets
;
-- Drop temporary PersonID column
不幸的是,Postgres INSERT
中的 RETURNING
子句似乎仅限于 returning 仅来自目标 table 的列,即只有那些值实际插入。例如,在 MS SQL 服务器 MERGE
中可以 return 来自源和目标 table 的值使此类任务变得容易,但我找不到类似的东西Postgres.
因此,没有在 Pet
table 中添加显式 PersonID
列的第二个变体需要将原始 Person
与新的 Pet
连接起来以映射旧的PersonID
到新 PetID
。
如果您的示例中可能存在重复项 (Cat Tom
),则使用 ROW_NUMBER
分配序号以区分重复行,如您在问题中所示。
如果没有这样的重复项,那么你可以简化映射并去掉ROW_NUMBER
。
INSERT INTO Pet (Pet, PetName)
SELECT Pet, PetName
FROM Person;
INSERT INTO PersonPet (FK_Person, FK_Pet)
SELECT
Person.ID AS FK_Person
,Pet.ID AS FK_Pet
FROM
Person
INNER JOIN Pet ON
Person.Pet = Pet.Pet AND
Person.PetName = Pet.PetName
;
我看到了第一种方法的一个优点。
如果您在 Pet
table 中显式存储 PersonID
,则分几步分批执行此类迁移会更容易。当 PersonPet
为空时,第二种变体工作正常,但如果您已经迁移了一批行,则过滤所需行可能会变得棘手。
您可以克服必须向宠物 table 添加额外列的限制,方法是先插入外键 table,然后再插入宠物 table。这允许首先确定映射是什么,然后在第二遍中填写详细信息。
INSERT INTO PersonPet
SELECT ID, nextval('pet_id_seq'::regclass) as PetID
FROM Person;
INSERT INTO Pet
SELECT FK_Pet, Pet, Petname
FROM Person join PersonPet on (ID=FK_Person);
这可以使用 Vladimir 在他的回答中概述的常见 table 表达机制组合成单个语句:
WITH
fkeys AS
(
INSERT INTO PersonPet
SELECT ID, nextval('pet_id_seq'::regclass) as PetID
FROM Person
RETURNING FK_Person as PersonID, FK_Pet as PetID
)
INSERT INTO Pet
SELECT f.PetID, p.Pet, p.Petname
FROM Person p join fkeys f on (p.ID=f.PersonID);
就优点和缺点而言:
您的解决方案 #1:
- 计算效率更高,它由两个扫描操作组成,没有连接也没有排序。
- 效率较低 space 因为它需要在 Pet table 中存储额外的数据。在 Postgres 中,space 未在 DROP 列上恢复(但您可以使用 CREATE TABLE AS / DROP TABLE 恢复它)。
- 如果你重复这样做可能会导致问题,例如adding/dropping一个列有规律的,因为你会运行进入Postgres的最大列限制。
我概述的解决方案的计算效率低于您的解决方案 #1,因为它需要连接,但比您的解决方案 #2 更有效。
实现您描述的效果的另一种解决方案(在我看来是最简单的;没有任何 CTE-s 或其他列):
create table Pet as
select
Id,
Pet,
PetName
from
Person;
create table PersonPet as
select
Id as FK_Person,
Id as FK_Pet
from
Person;
create sequence PetSeq;
update PersonPet set FK_Pet=nextval('PetSeq'::regclass);
update Pet p set Id=FK_Pet from PersonPet pp where p.Id=pp.FK_Person;
alter table Pet alter column Id set default nextval('PetSeq'::regclass);
alter table Pet add constraint PK_Pet primary key (Id);
alter table PersonPet add constraint FK_Pet foreign key (FK_Pet) references Pet(Id);
除非我们使用序列生成一个,否则我们只是使用现有的人 id 作为宠物的临时 id。
编辑
也可以使用我已经完成架构更改的方法:
insert into Pet(Id, Pet, PetName)
select
Id,
Pet,
PetName
from
Person;
insert into PersonPet(FK_Person, FK_Pet)
select
Id,
Id
from
Person;
select setval('PetSeq'::regclass, (select max(Id) from Person));
我想知道在将 Table 拆分为多对多关系时如何最好地迁移我的数据。我已经做了一个简化的例子,我也会 post 我想出的一些解决方案。 我正在使用 Postgresql 数据库。
迁移前
Table 人
ID Name Pet PetName
1 Follett Cat Garfield
2 Rowling Hamster Furry
3 Martin Cat Tom
4 Cage Cat Tom
迁移后
Table 人
ID Name
1 Follett
2 Rowling
3 Martin
4 Cage
Table宠物
ID Pet PetName
6 Cat Garfield
7 Hamster Furry
8 Cat Tom
9 Cat Tom
Table 人宠物
FK_Person FK_Pet
1 6
2 7
3 8
4 9
备注:
- 我将专门复制 Pet Table 中的条目(因为在我的情况下 - 由于其他相关数据 - 其中一个可能仍被客户编辑 table 而另一个可能不是).
- 没有唯一标识 "Pet" 记录的列。
- 对我来说,在 PersonPet table 或 3-9 和 4-8 中链接 3-8 和 4-9 并不重要。
- 我还省略了所有处理 table 模式更改的代码,因为据我所知,这与这个问题无关。
我的解决方案
- 创建宠物时 Table 临时添加一列,其中包含用于创建此条目的人物 Table 的 ID。
ALTER TABLE Pet ADD COLUMN IdPerson INTEGER;
INSERT INTO Pet (Pet, PetName, IdPerson)
SELECT Pet, PetName, ID
FROM Person;
INSERT INTO PersonPet (FK_Person, FK_Pet)
SELECT ID, IdPerson
FROM Pet;
ALTER TABLE Pet DROP Column IdPerson;
- 避免临时修改宠物table
INSERT INTO Pet (Pet, PetName)
SELECT Pet, PetName
FROM Person;
WITH
CTE_Person
AS
(SELECT
Id, Pet, PetName
,ROW_NUMBER() OVER (PARTITION BY Pet, PetName ORDER BY Id) AS row_number
FROM Person
)
,CTE_Pet
AS
(SELECT
Id, Pet, PetName
,ROW_NUMBER() OVER (PARTITION BY Pet, PetName ORDER BY Id) AS row_number
FROM Pet
)
,CTE_Joined
AS
(SELECT
CTE_Person.Id AS Person_Id,
CTE_Pet.Id AS Pet_Id
FROM
CTE_Person
INNER JOIN CTE_Pet ON
CTE_Person.Pet = CTE_Pet.Pet
CTE_Person.PetName = CTE_Pet.PetName
AND CTE_Person.row_number = CTE_Pet.row_number
)
INSERT INTO PersonPet (FK_Person, FK_Pet)
SELECT Person_Id, Pet_Id from CTE_Joined;
问题
- 两种解法都正确吗? (我已经测试了第二种解决方案,结果似乎是正确的,但我可能遗漏了一些极端情况)
- 这两个解的advantages/disadvantages是什么?
- 是否有更简单的方法来执行相同的数据迁移? (出于好奇,我也会对稍微修改我的约束的答案感兴趣(例如,Pet table 中没有重复的条目),但请指出哪些:))。
是的,您的两个解决方案都是正确的。他们让我想起了
几个笔记。
在 Pet
table 中添加额外列 PersonID
的第一个变体可以使用 RETURNING
子句在单个查询中完成。
-- Add temporary PersonID column to Pet
WITH
CTE_Pets
AS
(
INSERT INTO Pet (PersonID, Pet, PetName)
SELECT Person.ID, Person.Pet, Person.PetName
FROM Person
RETURNING ID AS PetID, PersonID
)
INSERT INTO PersonPet (FK_Person, FK_Pet)
SELECT PersonID, PetID
FROM CTE_Pets
;
-- Drop temporary PersonID column
不幸的是,Postgres INSERT
中的 RETURNING
子句似乎仅限于 returning 仅来自目标 table 的列,即只有那些值实际插入。例如,在 MS SQL 服务器 MERGE
中可以 return 来自源和目标 table 的值使此类任务变得容易,但我找不到类似的东西Postgres.
因此,没有在 Pet
table 中添加显式 PersonID
列的第二个变体需要将原始 Person
与新的 Pet
连接起来以映射旧的PersonID
到新 PetID
。
如果您的示例中可能存在重复项 (Cat Tom
),则使用 ROW_NUMBER
分配序号以区分重复行,如您在问题中所示。
如果没有这样的重复项,那么你可以简化映射并去掉ROW_NUMBER
。
INSERT INTO Pet (Pet, PetName)
SELECT Pet, PetName
FROM Person;
INSERT INTO PersonPet (FK_Person, FK_Pet)
SELECT
Person.ID AS FK_Person
,Pet.ID AS FK_Pet
FROM
Person
INNER JOIN Pet ON
Person.Pet = Pet.Pet AND
Person.PetName = Pet.PetName
;
我看到了第一种方法的一个优点。
如果您在 Pet
table 中显式存储 PersonID
,则分几步分批执行此类迁移会更容易。当 PersonPet
为空时,第二种变体工作正常,但如果您已经迁移了一批行,则过滤所需行可能会变得棘手。
您可以克服必须向宠物 table 添加额外列的限制,方法是先插入外键 table,然后再插入宠物 table。这允许首先确定映射是什么,然后在第二遍中填写详细信息。
INSERT INTO PersonPet
SELECT ID, nextval('pet_id_seq'::regclass) as PetID
FROM Person;
INSERT INTO Pet
SELECT FK_Pet, Pet, Petname
FROM Person join PersonPet on (ID=FK_Person);
这可以使用 Vladimir 在他的回答中概述的常见 table 表达机制组合成单个语句:
WITH
fkeys AS
(
INSERT INTO PersonPet
SELECT ID, nextval('pet_id_seq'::regclass) as PetID
FROM Person
RETURNING FK_Person as PersonID, FK_Pet as PetID
)
INSERT INTO Pet
SELECT f.PetID, p.Pet, p.Petname
FROM Person p join fkeys f on (p.ID=f.PersonID);
就优点和缺点而言:
您的解决方案 #1:
- 计算效率更高,它由两个扫描操作组成,没有连接也没有排序。
- 效率较低 space 因为它需要在 Pet table 中存储额外的数据。在 Postgres 中,space 未在 DROP 列上恢复(但您可以使用 CREATE TABLE AS / DROP TABLE 恢复它)。
- 如果你重复这样做可能会导致问题,例如adding/dropping一个列有规律的,因为你会运行进入Postgres的最大列限制。
我概述的解决方案的计算效率低于您的解决方案 #1,因为它需要连接,但比您的解决方案 #2 更有效。
实现您描述的效果的另一种解决方案(在我看来是最简单的;没有任何 CTE-s 或其他列):
create table Pet as
select
Id,
Pet,
PetName
from
Person;
create table PersonPet as
select
Id as FK_Person,
Id as FK_Pet
from
Person;
create sequence PetSeq;
update PersonPet set FK_Pet=nextval('PetSeq'::regclass);
update Pet p set Id=FK_Pet from PersonPet pp where p.Id=pp.FK_Person;
alter table Pet alter column Id set default nextval('PetSeq'::regclass);
alter table Pet add constraint PK_Pet primary key (Id);
alter table PersonPet add constraint FK_Pet foreign key (FK_Pet) references Pet(Id);
除非我们使用序列生成一个,否则我们只是使用现有的人 id 作为宠物的临时 id。
编辑
也可以使用我已经完成架构更改的方法:
insert into Pet(Id, Pet, PetName)
select
Id,
Pet,
PetName
from
Person;
insert into PersonPet(FK_Person, FK_Pet)
select
Id,
Id
from
Person;
select setval('PetSeq'::regclass, (select max(Id) from Person));