Вставьте данные и установите внешние ключи с Postgres - PullRequest
11 голосов
/ 12 сентября 2011

Мне нужно перенести большой объем существующих данных в базу данных 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;

Как мне распространить идентификаторы вставленных строк и установить ссылки на внешние ключи в таблице пользователей?

Единственное решение, которое я мог придумать, - это созданиевременный столбец user_id в таблице адресов с последующим обновлением address_id:

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

Однако это оказалось очень медленно (несмотря на использование индексов для users.id и address.user_id).

Таблица пользователей содержит около 3 миллионов строк, в которых 300 тыс. Пропущен связанный адрес.

Есть ли другой способ вставить производные данные в одну таблицу и задать ссылку на внешний ключ для вставленных данных?в другом (без изменения самой схемы)?

Я использую Postgres 8.3.14.

Спасибо

У меня сейчас solРешите проблему, перенеся данные с помощью скрипта Python / sqlalchemy.Это оказалось намного проще (для меня), чем пытаться сделать то же самое с SQL.Тем не менее, мне было бы интересно, если кто-нибудь знает способ обработки результата ВОЗВРАТА оператора INSERT в Postgres SQL.

1 Ответ

15 голосов
/ 14 апреля 2012

В таблице users должен быть некоторый первичный ключ , который вы не раскрыли.Для целей этого ответа я назову его users_id.

Вы можете решить эту проблему довольно элегантно с помощью CTE, модифицирующих данные , представленных в 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 в вашем вопросе.Если вы не нашли время для обновления, вы можете рассмотреть возможность обновления. Конец жизни скоро наступит для 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.

...