2011-09-12 18 views
8

Muszę przeprowadzić migrację dużej ilości istniejących danych w DB PostgreS po zmianie schematu.Wstawianie danych i ustawianie kluczy obcych za pomocą Postgreatora

W starym schemacie atrybut kraju będzie przechowywany w tabeli użytkowników. Teraz atrybut kraj został przeniesiony do osobnej tabeli adres:

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

addresses: 
    id 
    country 

Schemat jest rzeczywiście bardziej skomplikowane i adres zawiera więcej niż tylko kraju. Dlatego każdy użytkownik musi mieć swój własny adres (relacja 1: 1).

Podczas migracji danych, Mam problemy ustawienie kluczy obcych w tabeli użytkowników po włożeniu adresy:

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

jaki sposób propagować identyfikatory wstawionych wierszy i ustawić klucz obcy referencje w tabeli użytkowników?

Jedynym rozwiązaniem mogłem wymyślić do tej pory jest stworzenie tymczasowej kolumny user_id w tabeli adresy i następnie aktualizowanie się address_id:

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

Jednak okazało się to być bardzo powolna (mimo za pomocą indeksów na obu users.id i addresses.user_id).

Tabela użytkowników zawiera około 3 miliony wierszy z 300 tys. Brakujących adresów.

Czy istnieje inny sposób wstawiania danych pochodnych do jednej tabeli i ustawiania odwołania do klucza obcego do wstawionych danych w drugiej (bez zmiany samego schematu)?

Używam Postgres 8.3.14.

Dzięki

Mam teraz rozwiązać problem poprzez migrację danych ze skryptu Python/SQLAlchemy. Okazało się to znacznie łatwiejsze (dla mnie) niż próba tego samego z SQL. Mimo to byłbym zainteresowany, gdyby ktokolwiek znał sposób przetwarzania wyniku RETURNING instrukcji INSERT w Postgres SQL.

+0

To jest stare i rozwiązałeś to. Ale stosunek 1: 1 nie ma sensu w tym przypadku. Nie powinieneś zamiast tego tworzyć tabeli krajów? –

+0

Adres faktycznie zawiera ulicę, miasto, kod pocztowy, ... i kraj dla każdego użytkownika. Uprościłem to, aby było bardziej czytelne. – Pankrat

+0

Kraj, kod pocztowy, miasto, hrabstwo itd. Będą miały swoje własne tabele. To pozostawia ulicę, numer itd. Wciąż nie ma miejsca w osobnej tabeli dla tych, o ile dla każdego użytkownika nie jest możliwy więcej niż jeden adres. –

Odpowiedz

10

Tabela users musi zawierać klucz główny , którego użytkownik nie ujawnił. Na potrzeby tej odpowiedzi nazwałbym to users_id.

Można rozwiązać to raczej elegancko z data-modifying CTEs wprowadzonego z PostgreSQL 9,1:

Jeśli możemy założyć, że country jest wyjątkowy, cała operacja jest dość banalna:

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; 

można wspomnieć wersja 8.3 w swoim pytaniu. Jeśli w międzyczasie nie udało ci się uaktualnić, możesz rozważyć ulepszenie. End of life is coming soon for 8.3.

Bądź co bądź, jest to dość proste w wersji 8.3.Trzeba tylko dwa oświadczenia:

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; 

Jeśli country nie jest wyjątkowy, staje się większym wyzwaniem. Ty może wystarczy utworzyć jeden adres i link do niego wiele razy. Ale wspomniałeś o relacji 1: 1, która wyklucza takie dogodne rozwiązanie.

Dla wersji 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; 

Ponieważ nie ma sposobu, aby jednoznacznie przypisać dokładnie jeden id zwrócony z INSERT dla każdego użytkownika w zestawie z identycznym country, używam funkcji okna row_number() aby są wyjątkowe.

Nie tak prosto z wersją 8.3. Jednym z możliwych sposobów:

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 

Powtórz ten aż ostatnia wartość NULL zniknął z users.address_id.

+0

Wielkie dzięki! Nauczyłem się kilku nowych rzeczy z twojej odpowiedzi. I tak, w międzyczasie uaktualniliśmy do PostgreSQL 9.1. Pozdrawiam: – Pankrat

+0

@Pankrat: to dobra wiadomość - zarówno, że pomogło, i że można uaktualnić do 9.1. –

Powiązane problemy