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

时间:2015-10-16 09:00:53

标签: sql postgresql many-to-many database-migration

我想知道在将表拆分为多对多关系时如何最好地迁移数据。我已经做了一个简化的例子,我还会发布一些我想出的解决方案。 我正在使用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

表PersonPet


FK_Person     FK_Pet
1             6
2             7
3             8
4             9

注意:

  • 我将特别重复宠物表中的条目(因为在我的情况下 - 由于其他相关数据 - 其中一个可能仍然可由客户编辑,而另一个可能不是)。
  • 没有唯一标识“宠物”记录的列。
  • 对我而言,3-8和4-9是否在PersonPet表或3-9和4-8中链接无关紧要。
  • 此外,我省略了处理表格架构更改的所有代码,因为这是 - 在我的理解中 - 与此问题无关。

我的解决方案

  1. 创建宠物表时,会临时添加一个包含用于创建此条目的人员表ID的列。
  2. 
        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. 避免临时修改宠物表
    2. 
          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. 这两种解决方案有哪些优点/缺点?
      3. 是否有更简单的方法进行相同的数据迁移? (为了我的好奇心,我也会对稍微修改我的约束的答案感兴趣(例如Pet表中没有重复的条目),但请指出哪些:))。

3 个答案:

答案 0 :(得分:5)

实现您所描述效果的另一种解决方案(在我看来是最简单的一种;没有任何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));

答案 1 :(得分:3)

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

很少注意到。

PersonID表中添加额外列Pet的第一个变体可以使用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中RETURNING中的INSERT子句仅限于从目标表返回列,即只有那些实际插入的值。例如,在MS SQL Server中MERGE可以从源表和目标表返回值,这使得这类任务变得简单,但我在Postgres中找不到任何类似的东西。

因此,在PersonID表格中添加明确Pet列的第二个变体需要将原始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为空时,第二种变体可以正常工作,但如果您已经迁移了一批行,则过滤所需的行可能会变得很棘手。

答案 2 :(得分:3)

您可以通过首先插入外键表然后插入宠物表来克服必须向宠物表添加额外列的限制。这允许首先确定映射的内容,然后在第二遍中填写详细信息。

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更有效。