2011-09-12 30 views
8

我必須在模式更改後遷移Postgres數據庫中的大量現有數據。插入數據並使用Postgres設置外鍵

在舊模式中,country屬性將存儲在users表中。現在全國的屬性已被移動到一個單獨的地址表:

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和設置的外鍵用戶表中的引用?

唯一的解決辦法我能想出到目前爲止是創建在地址表中的臨時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上使用索引)。

用戶表包含大約300萬行,其中300k缺少關聯的地址。

是否有任何其他方式將派生數據插入到一個表中,並將插入數據的外鍵引用設置到另一個表中(而不更改模式本身)?

我使用的是Postgres 8.3.14。

感謝

現在我已經通過將數據與一個Python/SQLAlchemy的腳本遷移解決了這個問題。事實證明,對於我來說,要比使用SQL更容易。不過,如果有人知道在Postgres SQL中處理INSERT語句的RETURNING結果的方法,我會感興趣。

+0

這是舊的,你解決了它。但在這種情況下1:1的關係是沒有意義的。你不應該創建一個國家表嗎? –

+0

地址實際上包含每個用戶的街道,城市,郵編,...和國家。我只是簡化它,使其更具可讀性。 – Pankrat

+0

國家,郵政編碼,城市,縣等都將擁有自己的表格。這留下了街道,號碼等。在這些單獨的表中仍然沒有任何意義,除非每個用戶可能有多個地址。 –

回答

10

users必須有一些主鍵,你沒有透露。爲了這個答案的目的,我將其命名爲users_id

您可以data-modifying CTEs PostgreSQL的9.1介紹而優雅的解決這個問題:

如果我們可以假設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; 

至於有沒有辦法明確指定由INSERT返回給每一個用戶在一組具有相同country只有一個id,我用的窗函數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了。

+0

非常感謝!從你的回答中學到了不少新東西。是的,我們在此期間升級到了Postgres 9.1。乾杯 – Pankrat

+0

@Pankrat:這是個好消息 - 它既有幫助,也可以升級到9.1。 –