Может ли кто-нибудь объяснить причудливое поведение при выполнении миллионов ОБНОВЛЕНИЙ?

8

Может ли кто-нибудь объяснить мне это поведение? Я выполнил следующий запрос на Postgres 9.3, работающем изначально на OS X. Я пытался смоделировать какое-то поведение, когда размер индекса мог вырасти намного больше размера таблицы, и вместо этого нашел что-то еще более странное.

CREATE TABLE test(id int);
CREATE INDEX test_idx ON test(id);

CREATE FUNCTION test_index(batch_size integer, total_batches integer) RETURNS void AS $$
DECLARE
  current_id integer := 1;
BEGIN
FOR i IN 1..total_batches LOOP
  INSERT INTO test VALUES (current_id);
  FOR j IN 1..batch_size LOOP
    UPDATE test SET id = current_id + 1 WHERE id = current_id;
    current_id := current_id + 1;
  END LOOP;
END LOOP;
END;
$$ LANGUAGE plpgsql;

SELECT test_index(500, 10000);

Я позволил этой программе работать на моем локальном компьютере около часа, прежде чем я начал получать предупреждения о проблемах диска из OS X. Я заметил, что Postgres высасывает со своего локального диска около 10 МБ / с и что база данных Postgres потребляет общую сумму. 30 ГБ с моей машины. Я закончил тем, что отменил запрос. Несмотря на это, Postgres не вернул мне дисковое пространство, и я запросил базу данных для статистики использования со следующим результатом:

test=# SELECT nspname || '.' || relname AS "relation",
    pg_size_pretty(pg_relation_size(C.oid)) AS "size"
  FROM pg_class C
  LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace)
  WHERE nspname NOT IN ('pg_catalog', 'information_schema')
  ORDER BY pg_relation_size(C.oid) DESC
  LIMIT 20;

           relation            |    size
-------------------------------+------------
 public.test                   | 17 GB
 public.test_idx               | 14 GB

Однако выбор из таблицы не дал результатов.

test=# select * from test limit 1;
 id
----
(0 rows)

Выполнение 10000 пакетов по 500 означает 5 000 000 строк, что должно привести к довольно небольшому размеру таблицы / индекса (в масштабе МБ). Я подозреваю, что Postgres создает новую версию таблицы / индекса для каждого INSERT / UPDATE, что происходит с функцией, но это кажется странным. Вся функция выполняется транзакционно, и таблица была пуста для запуска.

Есть мысли о том, почему я вижу это поведение?

В частности, у меня есть два вопроса: почему эта область еще не была восстановлена ​​базой данных, и второй - почему база данных потребовала столько места в первую очередь? 30 ГБ кажется много, даже если учитывать MVCC

Нихил Н
источник

Ответы:

7

Укороченная версия

Ваш алгоритм выглядит O (n * m) на первый взгляд, но эффективно растет O (n * m ^ 2), потому что все строки имеют одинаковый идентификатор. Вместо 5M строк, вы получите> 1,25G строк

Длинная версия

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

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

Первая итерация внешнего цикла - current_id начинается с 1, вставляет 1 строку, затем внутренний цикл выполняет обновление 10000 раз для одной и той же строки, завершается единственной строкой с идентификатором 10001 и current_id со значением 10001. 10001 версии строки все еще сохраняются, так как транзакция не завершена.

Вторая итерация внешнего цикла - так как current_id равен 10001, новая строка вставляется с идентификатором 10001. Теперь у вас есть 2 строки с одинаковым «идентификатором» и 10003 версии в общей сложности обеих строк (10002 из первой, 1 из второй). Затем внутренний цикл обновляет 10000 раз ОБА рядов, создавая 20000 новых версий, получая до 30003 кортежей на данный момент ...

Третья итерация внешнего цикла: текущий идентификатор - 20001, новая строка вставлена ​​с идентификатором 20001. У вас есть 3 строки, все с одинаковым «идентификатором» 20001, 30006 строк / кортежей версий. Затем вы выполняете 10000 обновлений в 3 ряда, создавая 30000 новых версий, а теперь 60006 ...

...

(Если ваше пространство позволило) - 500-я итерация внешнего цикла, создает 5M обновлений 500 строк, только в этой итерации

Как вы видите, вместо ожидаемых 5M обновлений вы получили 1000 + 2000 + 3000 + ... + 4990000 + 5000000 обновлений (плюс изменение), что составит 10000 * (1 + 2 + 3 + ... + 499+ 500), более 1,25G обновлений. И, конечно, строка - это не просто размер вашего int, она требует дополнительной структуры, поэтому ваша таблица и индекс имеют размер более десяти гигабайт.

Связанные вопросы и ответы:

Бруно Гардиа
источник
5

PostgreSQL возвращает дисковое пространство только после VACUUM FULL, а не после DELETEили ROLLBACK(в результате отмены)

Стандартная форма VACUUM удаляет версии мертвых строк в таблицах и индексах и отмечает пространство, доступное для повторного использования в будущем. Однако он не вернет пространство операционной системе, за исключением особого случая, когда одна или несколько страниц в конце таблицы становятся полностью свободными, и можно легко получить эксклюзивную блокировку таблицы. Напротив, VACUUM FULL активно уплотняет таблицы, записывая полную новую версию файла таблицы без мертвого пространства. Это минимизирует размер таблицы, но может занять много времени. Также требуется дополнительное дисковое пространство для новой копии таблицы, пока операция не завершится.

Как примечание стороны, вся ваша функция кажется сомнительной. Я не уверен, что вы пытаетесь проверить, но если вы хотите создать данные, вы можете использоватьgenerate_series

INSERT INTO test
SELECT x FROM generate_series(1, batch_size*total_batches) AS t(x);
Эван Кэрролл
источник
Круто, это объясняет, почему таблица все еще была помечена как потребляющая столько данных, но зачем ей вообще требовалось все это пространство? Исходя из моего понимания MVCC, он должен поддерживать отдельные версии обновленных / вставленных кортежей для транзакции, но он не должен поддерживать отдельные версии для каждой итерации цикла.
Nikhil N
1
Каждая итерация цикла порождает новые кортежи.
Эван Кэрролл
2
Да, но у меня сложилось впечатление, что MVCC не должен создавать кортежи для всех кортежей, которые он изменял в ходе транзакции. То есть, когда первый INSERT запускается, Postgres создает один кортеж и добавляет один новый кортеж для каждого UPDATE. Так как ОБНОВЛЕНИЯ запускаются для каждой строки 500 раз, а вставок - 10000, это означает, что 500 * 10000 строк = 5М кортежей на момент фиксации транзакции. Теперь это только оценка, но независимо от 5M *, скажем, 50 байтов для отслеживания каждого набора ~ 250 МБ, что НАМНОГО меньше 30 ГБ. Откуда все это?
Nikhil N
Также re: сомнительная функция, я пытаюсь проверить поведение индекса, когда индексированные поля обновляются много раз, но монотонно увеличивающимся способом, таким образом получая очень разреженный индекс, но всегда добавляемый на диск.
Nikhil N
Я не понимаю, что вы думаете. Как вы думаете, строка, обновленная 18e раз в цикле - это один кортеж или 1e8 кортежей?
Эван Кэрролл
3

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

Когда мы запускаем его с параметрами nи m:

SELECT test_index(n, m);

Есть mстроки вставки и n * (m^2 + m) / 2обновления. Таким образом, для n = 500и m = 10000Postgres нужно будет вставить только 10K строк, но выполнить обновления кортежей ~ 25G (25 миллиардов).

Учитывая, что строка в Postgres имеет около 24 байтов служебной информации, таблице с одним intстолбцом потребуется 28 байтов на строку плюс накладные расходы страницы. Таким образом, для завершения операции нам потребуется около 700 ГБ плюс место для индекса (что также будет несколько сотен ГБ).


тестирование

Чтобы проверить теорию, мы создали еще одну таблицу test_testс одной строкой.

CREATE TABLE test_test (i int not null) ;
INSERT INTO test_test (i) VALUES (0);

Затем мы добавляем триггер, testчтобы каждое обновление увеличивало счетчик на 1. (Код опущен). Затем мы запускаем функцию, с меньшими значениями, n = 50и m = 100.

Наша теория предсказывает :

  • 100 рядных вставок,
  • Обновления кортежа 250K (252500 = 50 * 100 * 101/2)
  • не менее 7 МБ для таблицы на диске
  • (+ место для индекса)

Тест 1 (оригинальная testтаблица, с индексом)

    SELECT test_index(50, 100) ;

После завершения мы проверяем содержимое таблицы:

x=# SELECT COUNT(*) FROM test ;
 count 
-------
   100
(1 row)

x=# SELECT i FROM test_test ;
   i    
--------
 252500
(1 row)

И использование диска (запрос в разделе Размер индекса / Статистика использования в обслуживании индекса ):

tablename | indexname | num_rows | table_size | index_size | unique | number_of_scans | tuples_read 
----------+-----------+----------+------------+------------+--------+-----------------+-------------
test      | test_idx  |      100 | 8944 kB    | 5440 kB    | N      |           10001 |      505003
test_test |           |        1 | 8944 kB    |            | N      |                 |            

testТаблица используется почти 9MB для таблицы и 5МБ для индекса. Обратите внимание, что test_testтаблица использовала еще 9 МБ! Это ожидаемо, поскольку оно также прошло 250K обновлений (наш второй триггер обновлял одну строку test_testдля каждого обновления строки в test.)

Обратите внимание также на количество сканированных таблиц test(10 КБ) и число записей (500 КБ).

Тест 2 ( testтаблица без индекса)

Точно так же, как и выше, за исключением того, что таблица не имеет индекса.

tablename | indexname | num_rows | table_size | index_size | unique | number_of_scans | tuples_read 
----------+-----------+----------+------------+------------+--------+-----------------+-------------
 test        |        |      100 | 8944 kB    |            | N      |                 |            
 test_test   |        |        1 | 8944 kB    |            | N      |                 |            

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

Тест 3 (с нижним коэффициентом заполнения)

Пробовал с fillfactor 50 и минимально возможным, 10. Никаких улучшений вообще. Использование диска было практически идентично предыдущим тестам (которые использовали коэффициент заполнения по умолчанию, 100 процентов)

ypercubeᵀᴹ
источник