Привет, Хабр!

Несмотря на то, что на текущем месте работы взаимодействовать с 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'а индексов упали. В отдельной ветке я подготовил проект для демонстрации этой ошибки.

Ошибки в тестах на PostgreSQL 13.0
Ошибки в тестах на PostgreSQL 13.0

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