将表拆分为多对多关系:数据迁移
Posted
技术标签:
【中文标题】将表拆分为多对多关系:数据迁移【英文标题】:Split Table into many to many relationship: Data Migration 【发布时间】:2016-01-14 23:09:16 【问题描述】:我想知道在将表拆分为多对多关系时如何最好地迁移我的数据。我做了一个简化的例子,我也会发布一些我想出的解决方案。 我正在使用 Postgresql 数据库。
迁移前
桌人
ID Name Pet PetName
1 Follett Cat Garfield
2 Rowling Hamster Furry
3 Martin Cat Tom
4 Cage Cat Tom
迁移后
桌人
ID Name
1 Follett
2 Rowling
3 Martin
4 Cage
餐桌宠物
ID Pet PetName
6 Cat Garfield
7 Hamster Furry
8 Cat Tom
9 Cat Tom
桌人宠物
FK_Person FK_Pet
1 6
2 7
3 8
4 9
注意事项:
我将专门复制 Pet Table 中的条目(因为在我的情况下 - 由于其他相关数据 - 其中一个可能仍可供客户编辑,而另一个可能不能)。 没有唯一标识“宠物”记录的列。 对我来说,3-8 和 4-9 是在 PersonPet 表中链接还是在 3-9 和 4-8 中链接并不重要。 此外,我省略了处理表架构更改的所有代码,因为在我的理解中,这与这个问题无关。我的解决方案
-
在创建 Pet Table 时临时添加一列,其中包含用于创建此条目的 Person 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;
-
避免临时修改 Pet 表
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;
问题
-
两种解决方案都正确吗? (我已经测试了第二种解决方案,结果似乎是正确的,但我可能错过了一些极端情况)
这两种解决方案的优缺点是什么?
是否有更简单的方法来执行相同的数据迁移? (出于我的好奇心,我也会对稍微修改我的约束的答案感兴趣(例如 Pet 表中没有重复的条目),但请指出哪些 :))。
【问题讨论】:
【参考方案1】:实现您描述的效果的另一种解决方案(我认为最简单的一种;没有任何 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));
【讨论】:
嗨。我喜欢这个解决方案,它看起来真的很整洁!在我的情况下,这不是一个真正的选择,因为对架构的更改是在不同的地方处理的。 我编辑了我的帖子,并为架构已修改的情况提供了解决方案。基本上我们所要做的就是调整宠物的顺序,瞧:)。【参考方案2】:您可以通过先插入外键表然后再插入 pets 表来克服必须向 pets 表中添加额外列的限制。这允许首先确定映射是什么,然后在第二遍中填写详细信息。
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 在他的回答中概述的公用表表达式机制组合成一个语句:
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:
计算效率更高,它由两个扫描操作组成,没有连接和排序。 空间效率较低,因为它需要在 Pet 表中存储额外数据。在 Postgres 中,DROP 列上的空间没有恢复(但您可以使用 CREATE TABLE AS / DROP TABLE 恢复它)。 如果您重复执行此操作可能会导致问题,例如定期添加/删除列,因为您会遇到 Postgres 最大列限制。我概述的解决方案的计算效率低于您的解决方案 #1,因为它需要连接,但比您的解决方案 #2 更有效。
【讨论】:
【参考方案3】:是的,您的两个解决方案都是正确的。他们让我想起了this answer。
一些注释。
在Pet
表中添加额外列PersonID
的第一个变体可以使用RETURNING
子句在单个查询中完成。
SQL Fiddle
-- 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
子句似乎仅限于仅从目标表返回列,即仅返回实际插入的那些值。例如,在 MS SQL Server 中,MERGE
可以从源表和目标表中返回值,从而使此类任务变得容易,但我在 Postgres 中找不到类似的东西。
因此,第二个变体没有在Pet
表中添加显式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
;
我看到了第一种方法的一个优点。
如果您将PersonID
显式存储在Pet
表中,则分几个步骤分批执行这种迁移会更容易。当PersonPet
为空时,第二个变体可以正常工作,但如果您已经迁移了一批行,那么过滤所需的行可能会变得很棘手。
【讨论】:
以上是关于将表拆分为多对多关系:数据迁移的主要内容,如果未能解决你的问题,请参考以下文章
更改模型以添加“通过”关系以订购多对多字段 - Django 1.7 迁移修改