我必须在架构更改后在Postgres数据库中迁移大量现有数据。
在旧架构中,country属性将存储在users表中。现在,country属性已移至单独的地址表中:
users:
country # OLD
address_id # NEW [1:1 relation]
addresses:
id
country
架构实际上更复杂,地址不仅包含国家/地区。因此,每个用户都需要拥有自己的地址(1:1关系)。
迁移数据时,我在插入地址后在users表中设置外键时遇到问题:
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语句的RETURNING结果的方法,我会感兴趣。
答案 0 :(得分:13)
表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 版本。如果您在此期间没有进行升级,则可能需要考虑升级。 End of life is coming soon for 8.3.
尽管如此,对于版本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的关系,排除了这种方便的解决方案。
对于版本 9.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;
由于无法明确地将id
中返回的一个INSERT
明确地分配给具有相同country
的集合中的每个用户,因此我使用窗口函数row_number()
使它们独一无二。
版本 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
消失。