将 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

备注:

我的解决方案

  1. 创建宠物时 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;
  1. 避免临时修改宠物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;

问题

  1. 两种解法都正确吗? (我已经测试了第二种解决方案,结果似乎是正确的,但我可能遗漏了一些极端情况)
  2. 这两个解的advantages/disadvantages是什么?
  3. 是否有更简单的方法来执行相同的数据迁移? (出于好奇,我也会对稍微修改我的约束的答案感兴趣(例如,Pet table 中没有重复的条目),但请指出哪些:))。

是的,您的两个解决方案都是正确的。他们让我想起了 .

几个笔记。

Pet table 中添加额外列 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 子句似乎仅限于 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));