Найкращий спосіб заповнити новий стовпчик у великій таблиці?


33

У нас в Постгресі розміщена таблиця розміром 2,2 ГБ з 7 801 611 рядками. Ми додаємо до нього стовпчик uuid / guide, і мені цікаво, який найкращий спосіб заповнити цей стовпець (оскільки ми хочемо додати NOT NULLдо нього обмеження).

Якщо я правильно розумію Postgres, оновлення технічно є видаленням та вставкою, так що це в основному відновлення всієї таблиці 2,2 ГБ. Також у нас працює раб, тому ми не хочемо, щоб це відставало.

Чи є кращий спосіб, ніж писати сценарій, який повільно заповнює його з часом?


2
Ви вже виконували ALTER TABLE .. ADD COLUMN ...чи це відповідь також?
ypercubeᵀᴹ

Ще не виконували жодних модифікацій таблиці, просто на стадії планування. Я робив це раніше, додаючи стовпець, заповнюючи його, потім додаючи обмеження або індекс. Однак ця таблиця значно більша, і я переживаю за навантаження, блокування, тиражування тощо ...
Collin Peters

Відповіді:


45

Це дуже залежить від деталей ваших вимог.

Якщо у вас є достатньо вільного місця (принаймні 110% pg_size_pretty((pg_total_relation_size(tbl))) на диску і ви можете дозволити собі блокування спільного доступу на деякий час та ексклюзивний замок на дуже короткий час , тоді створіть нову таблицю, включаючи uuidстовпчик, використовуючи CREATE TABLE AS. Чому?

У наведеному нижче коді використовується функція додаткового uuid-ossмодуля .

  • Блокуйте таблицю проти одночасних змін у SHAREрежимі (все ще дозволяючи одночасні читання). Спроби записати в стіл зачекають і врешті-решт не вдасться. Дивись нижче.

  • Скопіюйте всю таблицю під час заповнення нового стовпця на льоту - можливо, впорядкувавши рядки, виходячи з неї.
    Якщо ви збираєтесь упорядкувати рядки, не забудьте встановити work_memмаксимальну висоту (лише для сеансу, а не в усьому світі).

  • Потім додайте до нової таблиці обмеження, зовнішні ключі, індекси, тригери тощо. Під час оновлення великих частин таблиці набагато швидше створювати індекси з нуля, ніж додавати рядки ітераційно.

  • Коли нова таблиця готова, киньте стару та перейменуйте нову, щоб зробити її заміною. Тільки цей останній крок набуває ексклюзивний замок на старій таблиці для решти транзакції - що зараз має бути дуже коротким.
    Він також вимагає видалити будь-який об’єкт залежно від типу таблиці (перегляди, функції, що використовують тип таблиці у підписі, ...) та відтворити їх згодом.

  • Робіть це за одну транзакцію, щоб уникнути неповних станів.

BEGIN;
LOCK TABLE tbl IN SHARE MODE;

SET LOCAL work_mem = '???? MB';  -- just for this transaction

CREATE TABLE tbl_new AS 
SELECT uuid_generate_v1() AS tbl_uuid, <list of all columns in order>
FROM   tbl
ORDER  BY ??;  -- optionally order rows favorably while being at it.

ALTER TABLE tbl_new
   ALTER COLUMN tbl_uuid SET NOT NULL
 , ALTER COLUMN tbl_uuid SET DEFAULT uuid_generate_v1()
 , ADD CONSTRAINT tbl_uuid_uni UNIQUE(tbl_uuid);

-- more constraints, indices, triggers?

DROP TABLE tbl;
ALTER TABLE tbl_new RENAME tbl;

-- recreate views etc. if any
COMMIT;

Це має бути найшвидшим. Будь-який інший спосіб оновлення на місці повинен також переписати всю таблицю, лише дорожче. Ви б пішли по цьому маршруту, лише якщо у вас недостатньо вільного місця на диску або не можете дозволити собі заблокувати всю таблицю або створити помилки для одночасних спроб запису.

Що відбувається з одночасними записами?

Інші транзакції (в інших сесіях), які намагаються INSERT/ UPDATE/ DELETEв тій же таблиці після того, як транзакція зняла SHAREблокування, будуть чекати, поки блокування буде відпущене або наступить тайм-аут, залежно від того, що відбудеться раніше. Вони не зможуть в будь-якому випадку, оскільки таблиця, в яку вони намагалися написати, була видалена з-під них.

У новій таблиці є нова таблиця OID, але паралельна транзакція вже вирішила ім'я таблиці до OID попередньої таблиці . Коли замок нарешті відпущений, вони намагаються самостійно заблокувати стіл, перш ніж писати на нього, і виявляють, що його немає. Postgres відповість:

ERROR: could not open relation with OID 123456

Де 123456OID старої таблиці. Вам потрібно знайти цей виняток і спробувати запити у коді програми, щоб уникнути цього.

Якщо ви не можете дозволити собі, щоб це сталося, вам доведеться зберегти свій оригінальний стіл.

Дві альтернативи збереження існуючої таблиці

  1. Оновіть на місці (можливо, запустіть оновлення на невеликих сегментах одночасно), перш ніж додати NOT NULLобмеження. Додавання нового стовпця зі значеннями NULL та без NOT NULLобмежень є дешевим.
    Оскільки Postgres 9.2 ви також можете створити CHECKобмеження за допомогоюNOT VALID :

    Обмеження все ще буде застосовано до наступних вставок або оновлень

    Це дозволяє оновлювати рядки peu à peu - у кількох окремих транзакціях . Це дозволяє уникнути блокування рядків занадто довго, а також дозволяє повторно використовувати мертві рядки. (Вам доведеться запустити VACUUMвручну, якщо між ними не буде достатньо часу для того, щоб почати автовакуум.) Нарешті, додайте NOT NULLобмеження та видаліть NOT VALID CHECKобмеження:

    ALTER TABLE tbl ADD CONSTRAINT tbl_no_null CHECK (tbl_uuid IS NOT NULL) NOT VALID;
    
    -- update rows in multiple batches in separate transactions
    -- possibly run VACUUM between transactions
    
    ALTER TABLE tbl ALTER COLUMN tbl_uuid SET NOT NULL;
    ALTER TABLE tbl ALTER DROP CONSTRAINT tbl_no_null;

    Відповідна відповідь, що обговорюється NOT VALIDбільш докладно:

  2. Підготуйте новий стан у тимчасовій таблиці , TRUNCATEоригінал та поповніть із таблиці темп. Все в одну транзакцію . Ще потрібно зняти SHAREблокування перед підготовкою нової таблиці, щоб запобігти втраті паралельних записів.

    Деталі у цій відповіді на SO:


Фантастична відповідь! Саме ту інформацію, яку я шукав. Два питання 1. Чи маєте ви якусь ідею про простий спосіб перевірити, як довго триватиме така дія? 2. Якщо потрібно сказати 5 хвилин, що відбувається з діями, які намагаються оновити рядок у цій таблиці протягом цих 5 хвилин?
Collin Peters

@CollinPeters: 1. Левова частка часу піде на копіювання великої таблиці - і, можливо, відтворення індексів та обмежень (це залежить). Скасування та перейменування коштує дешево. Для тестування ви можете запустити підготовлений сценарій SQL без LOCKдодання та виключення DROP. Я міг лише вимовляти дикі і марні здогадки. Щодо 2., будь ласка, розгляньте додаток до моєї відповіді.
Erwin Brandstetter

@ErwinBrandstetter Продовжуйте створювати перегляди, тому якщо у мене є десяток переглядів, які все-таки використовують стару таблицю (oid) після перейменування таблиці. Чи є якийсь спосіб виконати глибоку заміну, а не повторити оновлення / створення цілого перегляду?
CodeFarmer

@CodeFarmer: Якщо ви просто перейменуєте таблицю, представлення продовжують працювати з перейменованою таблицею. Щоб перегляди використовували нову таблицю замість них, потрібно відтворити їх на основі нової таблиці. (Також, щоб дозволити видалення старої таблиці.) Немає (практичного) способу її обходу.
Ервін Брандстеттер

14

Я не маю "найкращої" відповіді, але у мене є "найменш погана" відповідь, яка може дати вам можливість зробити справи досить швидко.

У моїй таблиці були рядки 2ММ, і продуктивність оновлення змінювалася, коли я намагався додати стовпчик вторинної часової позначки, який за замовчуванням став першим.

ALTER TABLE mytable ADD new_timestamp TIMESTAMP ;
UPDATE mytable SET new_timestamp = old_timestamp ;
ALTER TABLE mytable ALTER new_timestamp SET NOT NULL ;

Після того, як воно зависло 40 хвилин, я спробував це на невеликій партії, щоб зрозуміти, як довго це може зайняти - прогноз був близько 8 годин.

Прийнята відповідь, безумовно, краща - але ця таблиця широко використовується в моїй базі даних. Є кілька десятків таблиць, які FKEY на нього; Я хотів уникнути перемикання зовнішніх ключів на стільки таблиць. А далі є погляди.

Трохи пошуків документів, тематичних досліджень та StackOverflow, і у мене було "A-Ha!" мить. Слив був не в основній ОНОВЛЕННІ, а в усіх операціях INDEX. У моїй таблиці було 12 індексів - кілька для унікальних обмежень, кілька для прискорення планування запитів і кілька для повнотекстового пошуку.

Кожен рядок, який було ОНОВЛЕНО, не працював лише на DELETE / INSERT, але й накладні зміни змін кожного індексу та перевірки обмежень.

Моє рішення полягало в тому, щоб скинути кожен індекс і обмеження, оновити таблицю, а потім додати всі індекси / обмеження.

На написання транзакції SQL знадобилося близько 3 хвилин, яка зробила наступне:

  • ПОЧАТОК;
  • впали індекси / складові
  • оновлення таблиці
  • повторно додавати індекси / обмеження
  • КОМІТЕТ;

Сценарій зайняв 7 хвилин.

Прийнята відповідь, безумовно, краща і правильніша ... і практично виключає потребу в простоях. У моєму випадку, для використання цього рішення знадобилося б значно більше роботи "розробника", і у нас було 30-хвилинне вікно запланованого простою, в якому воно могло бути виконане. Наше рішення вирішило це в 10.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.