Как мне делать большие неблокирующие обновления в PostgreSQL? - PullRequest
60 голосов
/ 11 июля 2009

Я хочу сделать большое обновление таблицы в PostgreSQL, но мне не нужно поддерживать целостность транзакций на протяжении всей операции, потому что я знаю, что столбец, который я изменяю, не будет записываться в или прочитайте во время обновления. Я хочу знать, есть ли простой способ в консоли psql , чтобы сделать эти типы операций быстрее.

Например, допустим, у меня есть таблица с именем "orders" с 35 миллионами строк, и я хочу сделать это:

UPDATE orders SET status = null;

Чтобы не отвлекаться на обсуждение вне форума, давайте предположим, что все значения статуса для 35 миллионов столбцов в настоящее время установлены на одно и то же (ненулевое) значение, что делает индекс бесполезным.

Проблема с этим утверждением состоит в том, что для вступления в силу требуется очень много времени (исключительно из-за блокировки), и все измененные строки блокируются до полного обновления. Это обновление может занять 5 часов, тогда как что-то вроде

UPDATE orders SET status = null WHERE (order_id > 0 and order_id < 1000000);

может занять 1 минуту. Более 35 миллионов строк, выполнение вышеизложенного и разбивка его на 35 частей займет 35 минут и сэкономит мне 4 часа 25 минут.

Я мог бы разбить его еще дальше с помощью скрипта (используя псевдокод здесь):

for (i = 0 to 3500) {
  db_operation ("UPDATE orders SET status = null
                 WHERE (order_id >" + (i*1000)"
             + " AND order_id <" + ((i+1)*1000) " +  ")");
}

Эта операция может завершиться всего за несколько минут, а не 35.

Так что это сводится к тому, что я действительно спрашиваю. Я не хочу писать офигенный сценарий, чтобы ломать операции каждый раз, когда я хочу сделать одноразовое большое обновление, подобное этому. Есть ли способ выполнить то, что я хочу полностью в SQL?

Ответы [ 9 ]

36 голосов
/ 04 марта 2014

Столбец / Строка

... Мне не нужно поддерживать целостность транзакций через вся операция, потому что я знаю, что столбец, который я изменяю, не будет записываться или читаться во время обновления.

Любой UPDATE в Модель MVCC PostgreSQL записывает новую версию всю строку . Если параллельные транзакции изменяют любой столбец той же строки, возникают проблемы с параллелизмом, требующие много времени. Подробности в руководстве. Знание того же столбца не будет затронуто одновременными транзакциями, избегает некоторых возможных осложнений, но не других.

Индекс

Чтобы не отвлекаться на оффтоп, давайте предположим, что все значения статуса для 35 миллионов столбцов на данный момент установлены к тому же (ненулевому) значению, что делает индекс бесполезным.

При обновлении всей таблицы (или ее основных частей) Postgres никогда не использует индекс . Последовательное сканирование выполняется быстрее, когда необходимо прочитать все или большинство строк. Напротив: обслуживание индекса означает дополнительные расходы на UPDATE.

Performance

Например, допустим, у меня есть таблица с названием "заказы" с 35 миллионами строки, и я хочу сделать это:

UPDATE orders SET status = null;

Я понимаю, что вы стремитесь к более общему решению (см. Ниже). Но по адресу актуальный вопрос спросил: это можно решить за всего за миллисекунды , независимо от размера таблицы:

ALTER TABLE orders DROP column status
                 , ADD  column status text;

По документации:

Когда столбец добавляется с ADD COLUMN, все существующие строки в таблице инициализируются значением столбца по умолчанию (NULL, если нет DEFAULT оговорка уточняется). Если предложение DEFAULT отсутствует, это просто изменение метаданных ...

И

Форма DROP COLUMN физически не удаляет столбец, а просто делает его невидимым для операций SQL. Последующая вставка и обновление Операции в таблице будут хранить нулевое значение для столбца. Таким образом, удаление столбца происходит быстро, но не сразу размер таблицы на диске, так как пространство, занимаемое удаленным колонна не исправлена. Пространство будет исправлено со временем как существующие строки обновляются. (Эти заявления не применяются, когда сброс системы столбца oid; это делается с немедленной перепиской.)

Убедитесь, что у вас нет объектов в зависимости от столбца (ограничения внешнего ключа, индексы, представления, ...). Вы должны были бы удалить / воссоздать их. За исключением этого, крошечные операции над таблицей системного каталога pg_attribute выполняют свою работу. Требуется эксклюзивная блокировка на столе, которая может быть проблемой для большой параллельной нагрузки. Поскольку это займет всего несколько миллисекунд, у вас все должно быть в порядке.

Если у вас есть столбец по умолчанию, который вы хотите сохранить, добавьте его обратно в отдельной команде . Выполнение одной и той же команды немедленно применило бы ее ко всем строкам, аннулировав эффект. Затем вы можете обновить существующие столбцы в пакетах . Перейдите по ссылке на документацию и прочитайте Примечания в руководстве.

Общее решение

dblink было упомянуто в другом ответе. Это позволяет получить доступ к «удаленным» базам данных Postgres в неявных отдельных подключениях. «Удаленная» база данных может быть текущей, тем самым достигая «автономных транзакций» : то, что записывает функция в «удаленной» БД, фиксируется и не может быть откатлено.

Это позволяет запускать одну функцию, которая обновляет большую таблицу на более мелкие части, и каждая часть фиксируется отдельно. Позволяет избежать накладных расходов на транзакции для очень большого числа строк и, что более важно, снимает блокировки после каждой части. Это позволяет выполнять параллельные операции без большой задержки и снижает вероятность взаимных блокировок.

Если у вас нет одновременного доступа, это вряд ли полезно - кроме как избежать ROLLBACK после исключения. Также рассмотрим SAVEPOINT для этого случая.

Ответственность

Прежде всего, множество мелких транзакций на самом деле дороже. Это имеет смысл только для больших таблиц . Сладость зависит от многих факторов.

Если вы не уверены, что делаете: безопасным методом является одна транзакция . Чтобы это работало должным образом, параллельные операции на столе должны совпадать. Например: одновременная запись пишет может переместить строку в раздел, который предположительно уже обработан. Или одновременное чтение может увидеть противоречивые промежуточные состояния. Вы были предупреждены.

Пошаговые инструкции

Сначала необходимо установить дополнительный модуль dblink:

Настройка соединения с dblink очень сильно зависит от настройки вашего кластера БД и действующих политик безопасности. Это может быть сложно. Связанный позже ответ с более , как связаться с dblink :

Создайте FOREIGN SERVER и USER MAPPING в соответствии с инструкциями, чтобы упростить и упростить соединение (если у вас его уже нет). Предполагая serial PRIMARY KEY с или без некоторых пробелов.

CREATE OR REPLACE FUNCTION f_update_in_steps()
  RETURNS void AS
$func$
DECLARE
   _step int;   -- size of step
   _cur  int;   -- current ID (starting with minimum)
   _max  int;   -- maximum ID
BEGIN
   SELECT INTO _cur, _max  min(order_id), max(order_id) FROM orders;
                                        -- 100 slices (steps) hard coded
   _step := ((_max - _cur) / 100) + 1;  -- rounded, possibly a bit too small
                                        -- +1 to avoid endless loop for 0
   PERFORM dblink_connect('myserver');  -- your foreign server as instructed above

   FOR i IN 0..200 LOOP                 -- 200 >> 100 to make sure we exceed _max
      PERFORM dblink_exec(
       $$UPDATE public.orders
         SET    status = 'foo'
         WHERE  order_id >= $$ || _cur || $$
         AND    order_id <  $$ || _cur + _step || $$
         AND    status IS DISTINCT FROM 'foo'$$);  -- avoid empty update

      _cur := _cur + _step;

      EXIT WHEN _cur > _max;            -- stop when done (never loop till 200)
   END LOOP;

   PERFORM dblink_disconnect();
END
$func$  LANGUAGE plpgsql;

Звоните:

SELECT f_update_in_steps();

Вы можете параметризовать любую часть в соответствии с вашими потребностями: имя таблицы, имя столбца, значение, ... просто обязательно очистите идентификаторы, чтобы избежать внедрения SQL:

Об избежании пустого ОБНОВЛЕНИЯ:

4 голосов
/ 14 июля 2009

Вы должны делегировать этот столбец в другую таблицу, например:

create table order_status (
  order_id int not null references orders(order_id) primary key,
  status int not null
);

Тогда ваша операция установки статуса = NULL будет мгновенной:

truncate order_status;
3 голосов
/ 18 августа 2011

Я бы использовал CTAS:

begin;
create table T as select col1, col2, ..., <new value>, colN from orders;
drop table orders;
alter table T rename to orders;
commit;
3 голосов
/ 11 июля 2009

Прежде всего - вы уверены, что вам нужно обновить все строки?

Возможно, некоторые строки уже имеют status NULL?

Если так, то:

UPDATE orders SET status = null WHERE status is not null;

Что касается разделения изменений - это невозможно в чистом sql. Все обновления в одной транзакции.

Один из возможных способов сделать это в «чистом sql» - это установить dblink, подключиться к той же базе данных с помощью dblink, а затем выпустить много обновлений через dblink, но это кажется излишним для такой простой задачи.

Обычно просто добавление правильного where решает проблему. Если это не так - просто разделите его вручную. Написание скрипта - это слишком много - обычно вы можете сделать это простым однострочником:

perl -e '
    for (my $i = 0; $i <= 3500000; $i += 1000) {
        printf "UPDATE orders SET status = null WHERE status is not null
                and order_id between %u and %u;\n",
        $i, $i+999
    }
'

Я обернул здесь строки для удобства чтения, как правило, это одна строка. Вывод вышеуказанной команды может быть напрямую передан в psql:

perl -e '...' | psql -U ... -d ...

Или сначала в файл, а затем в psql (на случай, если вам понадобится файл позже):

perl -e '...' > updates.partitioned.sql
psql -U ... -d ... -f updates.partitioned.sql
2 голосов
/ 15 июля 2009

Вы уверены, что это из-за блокировки? Я так не думаю, и есть много других возможных причин. Чтобы узнать, вы всегда можете попробовать сделать только блокировку. Попробуй это: НАЧАТЬ; ВЫБРАТЬ СЕЙЧАС (); ВЫБЕРИТЕ * ОТ ЗАКАЗА ДЛЯ ОБНОВЛЕНИЯ; ВЫБРАТЬ СЕЙЧАС (); ROLLBACK;

Чтобы понять, что на самом деле происходит, вы должны сначала выполнить EXPLAIN (статус SET EXPLAIN UPDATE orders ...) и / или EXPLAIN ANALYZE. Возможно, вы обнаружите, что у вас недостаточно памяти для эффективного ОБНОВЛЕНИЯ. Если это так, SET work_mem TO 'xxxMB'; может быть простым решением.

Кроме того, следите за журналом PostgreSQL, чтобы увидеть, возникают ли проблемы, связанные с производительностью.

2 голосов
/ 11 июля 2009

Я ни в коем случае не администратор баз данных, но дизайн базы данных, где вам часто приходится обновлять 35 миллионов строк, может иметь ... проблемы.

Простое WHERE status IS NOT NULL может немного ускорить процесс (при условии, что у вас есть индекс состояния) - не зная фактического варианта использования, я предполагаю, что если это часто выполняется, значительная часть из 35 миллионов строк возможно, уже имеет нулевой статус.

Однако вы можете создавать циклы внутри запроса с помощью оператора LOOP . Я просто приготовлю небольшой пример:

CREATE OR REPLACE FUNCTION nullstatus(count INTEGER) RETURNS integer AS $$
DECLARE
    i INTEGER := 0;
BEGIN
    FOR i IN 0..(count/1000 + 1) LOOP
        UPDATE orders SET status = null WHERE (order_id > (i*1000) and order_id <((i+1)*1000));
        RAISE NOTICE 'Count: % and i: %', count,i;
    END LOOP;
    RETURN 1;
END;
$$ LANGUAGE plpgsql;

Затем его можно запустить, выполнив что-то похожее на:

SELECT nullstatus(35000000);

Возможно, вы захотите выбрать количество строк, но имейте в виду, что точное количество строк может занять много времени. В вики PostgreSQL есть статья о медленном подсчете и о том, как его избежать .

Кроме того, часть RAISE NOTICE предназначена только для отслеживания того, как далеко находится сценарий. Если вы не отслеживаете уведомления или не заботитесь о них, лучше не указывать их.

2 голосов
/ 11 июля 2009

Postgres использует MVCC (управление несколькими версиями параллелизма), таким образом, избегая любой блокировки, если вы единственный писатель; любое количество одновременно работающих считывателей может работать на столе, и не будет никакой блокировки.

Таким образом, если это действительно занимает 5 часов, это должно быть по другой причине (например, что у вас есть одновременные записи, в отличие от вашего утверждения, что вы этого не делаете).

1 голос
/ 23 ноября 2017

Некоторые опции, которые не были упомянуты:

Используйте трюк new table . Вероятно, в вашем случае вы должны написать несколько триггеров для обработки, чтобы изменения в исходной таблице также распространялись на вашу копию таблицы, что-то вроде этого ... ( percona - пример что-то, что делает это триггерным способом). Другим вариантом может быть «создать новый столбец, а затем заменить им старый» трюк , чтобы избежать блокировок (неясно, помогает ли скорость).

Возможно, рассчитайте максимальный идентификатор, затем сгенерируйте «все запросы, которые вам нужны» и передайте их как один запрос, такой как update X set Y = NULL where ID < 10000 and ID >= 0; update X set Y = NULL where ID < 20000 and ID > 10000; ..., тогда он может не так сильно блокировать и все равно будет весь SQL, хотя у вас есть дополнительные логика заранее, чтобы сделать это: (

0 голосов
/ 29 мая 2019

PostgreSQL версии 11 автоматически обрабатывает это с помощью Fast ALTER TABLE ADD COLUMN с функцией по умолчанию, отличной от NULL. Пожалуйста, сделайте обновление до версии 11, если это возможно.

Объяснение в этом блоге .

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...