Сдутие таблиц без блокировки
Братишка прислал ссылку на интересную статью — «Reducing bloat without locking». Речь идёт о так называемом «распухании» (bloating) таблиц в «Постгресе».
Подробно суть в трёх предложениях не объяснить, если вкратце, то механизм изоляции в транзакциях у «Постгреса» устроен так, что при обновлении и удалении строк в таблицах могут образовываться «дыры», которые увеличивают общий объём таблицы. Если происходят вставки или обновления и новые данные помещаются в неиспользуемое пространство, то «дыры» могут со временем заполняться. Но иногда «дыр» остаётся много и это проблема с которой надо что-то делать.
Вот например создадим таблицу из тысячи строк, посмотрим сколько страниц она занимает, потом удалим половину строк и снова посмотрим количество занятых страниц:
test=> CREATE TABLE test AS SELECT generate_series(1, 1000) id;
SELECT 1000
test=> SELECT MAX(ctid) FROM test;
max
--------
(4,96)
(1 row)
test=> DELETE FROM test WHERE id & 1 = 1;
DELETE 500
test=> SELECT MAX(ctid) FROM test;
max
--------
(4,96)
(1 row)
ctid — это специальный внутренний столбец «Постгреса», который отображает физическое расположение строки. Для каждой строки в нём записано два числа — номер страницы, где располагается строка и порядковый номер строки в странице.
Как видим, таблица «распухла» — половина строк удалена, а количество занимаемых страниц не изменилось. Что же можно сделать? Давайте попробуем команду VACUUM:
test=> VACUUM VERBOSE test;
INFO: vacuuming "public.test"
INFO: "test": found 0 removable, 500 nonremovable row versions in 5 out of 5 pages
DETAIL: 0 dead row versions cannot be removed yet.
There were 500 unused item pointers.
Skipped 0 pages due to buffer pins.
0 pages are entirely empty.
CPU 0.00s/0.00u sec elapsed 0.00 sec.
VACUUM
Увы, ничего не изменилось — она умеет урезать страницы только если они находятся в конце и полностью свободны.
Что же делать? Можно выполнить VACUUM FULL — эта команда полностью перельёт таблицу на новое место, избавив нас от «дыр», но во время работы таблица будет эксклюзивно заблокирована, для больших таблиц, да ещё в продуктиве это непозволительно.
Другой способ — перенести строки с последних страниц в «дыры» на первых, освободив таким образом несколько страниц в конце. Попробуем:
test=> BEGIN;
BEGIN
test=> UPDATE test SET id=id WHERE ctid>='(3,0)';
UPDATE 161
test=> UPDATE test SET id=id WHERE ctid>='(3,0)';
UPDATE 161
test=> UPDATE test SET id=id WHERE ctid>='(3,0)';
UPDATE 48
test=> UPDATE test SET id=id WHERE ctid>='(3,0)';
UPDATE 48
test=> UPDATE test SET id=id WHERE ctid>='(3,0)';
UPDATE 34
test=> UPDATE test SET id=id WHERE ctid>='(3,0)';
UPDATE 0
test=> COMMIT;
COMMIT
В одной транзакции мы обновили все строки, которые располагаются на первой странице и дальше. Попробуем теперь сделать VACUUM:
test=> VACUUM VERBOSE test;
INFO: vacuuming "public.test"
INFO: "test": removed 452 row versions in 2 pages
INFO: "test": found 452 removable, 500 nonremovable row versions in 5 out of 5 pages
DETAIL: 0 dead row versions cannot be removed yet.
There were 469 unused item pointers.
Skipped 0 pages due to buffer pins.
0 pages are entirely empty.
CPU 0.00s/0.00u sec elapsed 0.00 sec.
INFO: "test": truncated 5 to 3 pages
DETAIL: CPU 0.00s/0.00u sec elapsed 0.00 sec.
VACUUM
Как видимо команда удалила две неиспользуемые страницы и уменьшила общее количество страниц с пяти до трёх.
Идея, описанная в статье, лежит в основе скрипта pgcompacttable из инструментария компании «Дейта Игрет».
А то, что строки переносятся, хотя данные в них не изменились (id=id), — баг или фича?
Фича. Иначе надо будет сосчитать строку таблицы, сравнить значения — это обращения к диску и проц. мощность. Считается, что об этом программист должен заботиться. Но если программисту лениво, можно попросить Постгрес это делать, такой механизм есть :)
Считывать строку таблицы в любом случае придется, а стоимость сравнения на порядок меньше стоимости записи.
Тем не менее, считается, что это забота программиста. Поэтому СУБД не занимается лишней работой. Повторюсь, если хочется, можно включить такую проверку.
В таблице, к слову, может быть много столбцов, в них могут быть сложные типы, которые хранятся в туплах. Это всё не очень-то просто проверять.