使用 Postgres 插入数据并设置外键

Posted

技术标签:

【中文标题】使用 Postgres 插入数据并设置外键【英文标题】:Insert data and set foreign keys with Postgres 【发布时间】:2011-11-15 12:09:27 【问题描述】:

架构更改后,我必须迁移 Postgres 数据库中的大量现有数据。

在旧模式中,国家属性将存储在用户表中。现在国家属性已经被移动到一个单独的地址表中:

users:
  country # OLD
  address_id # NEW [1:1 relation]

addresses:
  id
  country

架构实际上更复杂,地址不仅包含国家/地区。因此,每个用户都需要有自己的地址(1:1 关系)。

迁移数据时,我在插入地址后在用户表中设置外键时遇到问题:

INSERT INTO addresses (country) 
    SELECT country FROM users WHERE address_id IS NULL 
    RETURNING id;

如何传播插入行的 ID 并在 users 表中设置外键引用?

到目前为止,我能想到的唯一解决方案是在地址表中创建一个临时 user_id 列,然后更新 address_id:

UPDATE users SET address_id = a.id FROM addresses AS a 
    WHERE users.id = a.user_id;

但是,结果证明这非常慢(尽管在 users.id 和addresses.user_id 上都使用了索引)。

users 表包含大约 300 万行,其中 300k 缺少关联地址。

有没有其他方法可以将派生数据插入到一个表中,并将外键引用设置为另一个表中插入的数据(不更改架构本身)?

我正在使用 Postgres 8.3.14。

谢谢

我现在已经通过使用 Python/sqlalchemy 脚本迁移数据解决了这个问题。结果证明(对我来说)比用 SQL 尝试同样的方法要容易得多。不过,如果有人知道在 Postgres SQL 中处理 INSERT 语句的返回结果的方法,我会很感兴趣。

【问题讨论】:

这是旧的,你解决了它。但是在这种情况下,1:1 的关系是没有意义的。您不应该创建一个国家/地区表吗? 地址实际上包含每个用户的街道、城市、邮政编码、...和国家。我只是对其进行了简化以使其更具可读性。 国家、邮政编码、城市、县等都有自己的表格。剩下街道、号码等。除非每个用户可能有多个地址,否则在单独的表中仍然没有任何意义。 【参考方案1】:

users 必须有一些您没有透露的主键。出于此答案的目的,我将其命名为users_id

您可以使用 PostgreSQL 9.1 引入的data-modifying CTEs 相当优雅地解决这个问题:

country 是独一无二的

在这种情况下,整个操作相当简单:

WITH i AS (
    INSERT INTO addresses (country) 
    SELECT country
    FROM   users
    WHERE  address_id IS NULL 
    RETURNING id, country
    )
UPDATE users u
SET    address_id = i.id
FROM   i
WHERE  i.country = u.country;

您在问题中提到了版本 8.3。升级! Postgres 8.3 has reached end of life.

尽管如此,这对于 8.3 版来说已经足够简单了。你只需要两个语句:

INSERT INTO addresses (country) 
SELECT country
FROM   users
WHERE  address_id IS NULL;

UPDATE users u
SET    address_id = a.id
FROM   addresses a
WHERE  address_id IS NULL 
AND    a.country = u.country;

country 不是唯一的

这更具挑战性。您可以只创建一个地址并多次链接到它。但是您确实提到了排除这种方便解决方案的 1:1 关系。

WITH s AS (
    SELECT users_id, country
         , row_number() OVER (PARTITION BY country) AS rn
    FROM   users
    WHERE  address_id IS NULL 
    )
    , i AS (
    INSERT INTO addresses (country) 
    SELECT country
    FROM   s
    RETURNING id, country
    )
    , r AS (
    SELECT *
         , row_number() OVER (PARTITION BY country) AS rn
    FROM   i
    )
UPDATE users u
SET    address_id = r.id
FROM   r
JOIN   s USING (country, rn)    -- select exactly one id for every user
WHERE  u.users_id = s.users_id
AND    u.address_id IS NULL;

由于无法明确地将从INSERT 返回的一个id 准确地分配给具有相同country 的集合中的每个用户,因此我使用窗口函数row_number() 使它们唯一。

Postgres 8.3 没有那么直接。一种可能的方法:

INSERT INTO addresses (country) 
SELECT DISTINCT country -- pick just one per set of dupes
FROM   users
WHERE  address_id IS NULL;

UPDATE users u
SET    address_id = a.id
FROM   addresses a
WHERE  a.country = u.country
AND    u.address_id IS NULL
AND NOT EXISTS (
    SELECT * FROM addresses b
    WHERE  b.country = a.country
    AND    b.users_id < a.users_id
    ); -- effectively picking the smallest users_id per set of dupes

重复此操作,直到最后一个 NULL 值从 users.address_id 消失。

【讨论】:

非常感谢!从你的回答中学到了很多新东西。是的,我们同时升级到 Postgres 9.1。干杯 @Pankrat:这是个好消息 - 既有帮助,又可以升级到 9.1。

以上是关于使用 Postgres 插入数据并设置外键的主要内容,如果未能解决你的问题,请参考以下文章

sql。两个间的外键约束和插入数据问题

将数据插入强规范化数据库并保持完整性(Postgres)

CoreData:从字典数组插入到SQLite数据库中发生未排序 - 因此无法设置外键

如何在 postgres 中创建表并插入具有动态值的数据

mysql表中,表的外键关联自身主键,为啥插入不了数据?

尝试将行插入表因为带有sequelize.js的UUID外键时出错