将表拆分为多对多关系:数据迁移

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 为空时,第二个变体可以正常工作,但如果您已经迁移了一批行,那么过滤所需的行可能会变得很棘手。

【讨论】:

以上是关于将表拆分为多对多关系:数据迁移的主要内容,如果未能解决你的问题,请参考以下文章

为多对多关系覆盖核心数据设置器方法

Rails 迁移:同一个类之间的多对多关系 |人际关系

更改模型以添加“通过”关系以订购多对多字段 - Django 1.7 迁移修改

如何将现有的一对多关系迁移到 Rails 和 ActiveRecord 中的多对多

实体框架将纯连接查找表转换为多对多关系

将 jQuery 数据从 View 传递到 Controller 并将其保存为多对多