Лучший способ заполнить новый столбец в большой таблице?

33

У нас в Postgres есть таблица объемом 2,2 ГБ с 7 801 611 строками. Мы добавляем к нему столбец uuid / guid, и мне интересно, как лучше заполнить этот столбец (поскольку мы хотим добавить NOT NULLк нему ограничение).

Если я правильно понимаю Postgres, обновление - это технически удаление и вставка, так что это в основном перестраивает всю таблицу 2.2 ГБ. Также у нас работает раб, поэтому мы не хотим, чтобы это отставало.

Есть ли способ лучше, чем написать сценарий, который постепенно заполняет его?

Коллин Питерс
источник
2
Вы уже выполнили ALTER TABLE .. ADD COLUMN ...или на эту часть тоже нужно ответить?
ypercubeᵀᴹ
Пока не выполняйте никаких модификаций таблицы, просто на стадии планирования. Я делал это раньше, добавляя столбец, заполняя его, затем добавляя ограничение или индекс. Тем не менее, эта таблица значительно больше, и я беспокоюсь о загрузке, блокировке, репликации и т. Д.
Коллин Питерс

Ответы:

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 предыдущей таблицы . Когда блокировка наконец снята, они пытаются заблокировать таблицу самостоятельно, прежде чем писать в нее, и обнаруживают, что она исчезла. Постгрес ответит:

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 минут?
Коллин Питерс
@CollinPeters: 1. Львиная доля времени уйдет на копирование большой таблицы - и, возможно, воссоздание индексов и ограничений (это зависит). Удаление и переименование дешево. Для тестирования вы можете запустить подготовленный сценарий SQL без и LOCKдо DROP. Я мог только произносить дикие и бесполезные догадки. Что касается 2., пожалуйста, рассмотрите дополнение к моему ответу.
Эрвин Брандштеттер
@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 на него; Я хотел избежать переключения FOREIGN KEYS на очень многих столах. И тогда есть взгляды.

Немного поиска документов, тематических исследований и StackOverflow, и у меня было "A-Ha!" момент. Утечка была не в основном UPDATE, а во всех операциях INDEX. В моей таблице было 12 индексов - несколько для уникальных ограничений, несколько для ускорения планировщика запросов и несколько для полнотекстового поиска.

Каждая строка, которая была ОБНОВЛЕНА, не только работала над DELETE / INSERT, но также и накладными расходами на изменение каждого индекса и проверку ограничений.

Мое решение состояло в том, чтобы удалить все индексы и ограничения, обновить таблицу, а затем добавить все индексы / ограничения обратно.

Потребовалось около 3 минут, чтобы написать транзакцию SQL, которая сделала следующее:

  • НАЧАТЬ;
  • пропущенные индексы / содержания
  • обновить таблицу
  • повторно добавить индексы / ограничения
  • COMMIT;

Выполнение сценария заняло 7 минут.

Принятый ответ определенно лучше и правильнее ... и практически исключает необходимость простоев. В моем случае, однако, потребовалось бы значительно больше работы «Разработчика», чтобы использовать это решение, и у нас было 30-минутное окно запланированного простоя, в котором оно могло бы быть достигнуто. Наше решение решило эту проблему в 10.

Джонатан Ванаско
источник