Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Привет Хабр!
Несмотря на то, что на текущем месте работы взаимодействовать с PostgreSQL приходится значительно меньше, я продолжаю поддерживать свою библиотеку pg-index-health. Недавно я зарелизил версию 0.3.1, ключевой особенностью которой стала поддержка PostgreSQL 13.
Эта история началась достаточно давно, ещё в октябре 2020. В процессе мне пришлось отказаться от embedded версии СУБД в тестах, перейти на Testcontainers, столкнуться с ошибкой в PG13 и даже немного пообщаться в переписке с одним из разработчиков Постгреса... Но обо всем по порядку.
Изначально для тестирования библиотеки я использовал embedded PostgreSQL. Я уже рассказывал об этом ранее. В целом это был неплохой вариант, но он завязан на чужие бинарники и их релизный цикл. Артефакта с 13-й версией долгое время не было (зарелизили только 13 ноября, спустя полтора месяца с момента начала моих экспериментов). Очевидно, что дальше жить с таким вариантом было крайне неудобно.
Альтернативный подход тоже был известен – Testcontainers (спасибо Николаю Кондратьеву за всю подготовительную работу), но мне до последнего момента не хотелось на него переходить. Дело в том, что Testcontainers — это дополнительный достаточно толстый слой между приложением и базой, запускаемой в Docker-контейнере. На MacOS и Windows такое решение время от времени ломается. И я столкнулся с этим. Дважды. Сначала на MacOS, затем на Windows.
Но что более печально, в Github Actions тесты с использованием Testcontainers можно запускать только на Ubuntu. По крайней мере у меня не получилось сделать это для Windows и MacOS. Если вы знаете, как настроить такую связку, буду премного благодарен за совет.
В какой-то момент времени все тесты в моём проекте оказались переведены на Testcontainers и успешно проходили на 11-й и 12-й версиях. Затем я переключил проект на ванильный образ с 13.0... И тут меня ждал облом... Тесты на оценку bloat'а индексов упали. В отдельной ветке я подготовил проект для демонстрации этой ошибки.
Первичный анализ ничего не дал. Никаких принципиальных изменений, которые бы ломали обратную совместимость между 12-й и 13-й версиями не было. Углублённое чтение release notes познакомило меня со сжатием дубликатов в b-tree индексах в новой версии - фичей, которую я ранее как-то совсем упустил (да, так тоже бывает).
Возможно, эти доработки и стали причиной изменившегося поведения (а может и нет - на детальное изучение кодовой базы PostgreSQL у меня не было времени и сил). Но вот "баг это или фича" и как теперь правильно оценивать bloat индексов, я не понимал. В открытых источниках ничего полезного найти не удалось.
И тогда я решил написать одному из соавторов доработок по сжатию дубликатов в b-tree индексах - Анастасии Лубенниковой. И… нет, здесь happy end'а не случилось: у Анастасии не было для меня готового решения, а потом навалилась работа, и на долгих два месяца я забросил проект...
Вернуться к PG 13 удалось только в начале января 2021. И я решил попробовать недавно вышедшее минорное обновление 13.1. Представьте себе моё удивление и радость, когда на этой версии все тесты прошли. На 13.1 оценка bloat'а индексов снова стала работать корректно!
Мораль этой истории проста и банальна: не стоит тащить новую версию любимой СУБД сразу в production. Ошибки бывают везде. Главное, чтобы они оперативно исправлялись. Ну а лично для меня приятным открытием стало то, что моё детище можно использовать как инструмент для acceptance testing (вернее, как его небольшую часть).
А что там со сжатием дубликатов в b-tree, спросите вы? Может быть, оно способно полностью решить проблему с null value в индексах?.. Увы, нет.
Сжатие дубликатов работает хорошо, даже замечательно. Вы можете проверить это самостоятельно. По ссылке подготовлен скрипт для инициализации и наполнения небольшой тестовой БД и команды для запуска двух разных версий СУБД в контейнере.
Init-db скрипт
create table if not exists test
(
id bigserial primary key,
fld varchar(255),
mark varchar(255),
nil varchar(255)
);
insert into test
select data.id, case when data.id % 2 = 0 then now()::text else null end, case when data.id % 2 = 0 then 'test_string'::text else null end, null
from generate_series(1, 100000) as data(id);
create index if not exists i_test_fld_with_nulls on test (fld);
create index if not exists i_test_fld_without_nulls on test (fld) where fld is not null;
create index if not exists i_test_mark_with_nulls on test (mark);
create index if not exists i_test_mark_without_nulls on test (mark) where mark is not null;
create index if not exists i_test_nil_with_nulls on test (nil);
create index if not exists i_test_nil_without_nulls on test (nil) where nil is not null;
Команды для запуска в Docker
docker run --name postgres-13 -e POSTGRES_USER=testuser -e POSTGRES_PASSWORD=testpwd -e POSTGRES_DB=testdb -d -p 5432:5432 -v /absolute/path/to/initdb.sql:/docker-entrypoint-initdb.d/initdb.sql postgres:13.2
docker run --name postgres-12 -e POSTGRES_USER=testuser -e POSTGRES_PASSWORD=testpwd -e POSTGRES_DB=testdb -d -p 6432:5432 -v /absolute/path/to/initdb.sql:/docker-entrypoint-initdb.d/initdb.sql postgres:12.6
psql -U testuser -d testdb
SQL запрос для оценки размера индексов
select
x.indrelid::regclass as table_name,
x.indexrelid::regclass as index_name,
pg_size_pretty(pg_relation_size(x.indexrelid)) as index_size
from pg_index x
join pg_stat_all_indexes psai on x.indexrelid = psai.indexrelid and psai.schemaname = 'public'
order by 1,2;
На выходе получим следующее:
Результаты в текстовом виде
PostgreSQL 12
table_name | index_name | index_size |
test | test_pkey | 2208 kB |
test | i_test_fld_with_nulls | 3552 kB |
test | i_test_fld_without_nulls | 2456 kB |
test | i_test_mark_with_nulls | 2664 kB |
test | i_test_mark_without_nulls | 1568 kB |
test | i_test_nil_with_nulls | 2224 kB |
test | i_test_nil_without_nulls | 8192 bytes |
PostgreSQL 13
table_name | index_name | index_size |
test | test_pkey | 2208 kB |
test | i_test_fld_with_nulls | 704 kB |
test | i_test_fld_without_nulls | 368 kB |
test | i_test_mark_with_nulls | 696 kB |
test | i_test_mark_without_nulls | 360 kB |
test | i_test_nil_with_nulls | 696 kB |
test | i_test_nil_without_nulls | 8192 bytes |
Обратите внимание, что размер индекса по первичному ключу, где все значения уникальны, не изменился.
Как видите, выбрасывать null value из индекса целиком по-прежнему крайне выгодно, и этой диагностикой не стоит пренебрегать.
Ну и напоследок. Embedded PG уходит в прошлое. Сейчас стоит отдавать предпочтение более современным и совершенным решениям наподобие Testcontainers.