Ускорить создание частичного индекса Postgres

8

Я пытаюсь создать частичные индексы для большой (1,2 ТБ) статической таблицы в Postgres 9.4.

Мои данные полностью статичны, поэтому я могу вставить все данные, а затем создать все индексы.

В этой таблице размером 1,2 ТБ у меня есть столбец, run_idкоторый четко разделяет данные. Мы добились отличной производительности, создав индексы, которые охватывают диапазон run_ids. Вот пример:

CREATE INDEX perception_run_frame_idx_run_266_thru_270
ON run.perception
(run_id, frame)
WHERE run_id >= 266 AND run_id <= 270;

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

Похоже, мы ограничены процессором ( topпоказывает 100% для процесса).
Что я могу сделать, чтобы ускорить создание наших частичных индексов?

Системные характеристики:

  • 18 ядро ​​Xeon
  • ОЗУ 192 ГБ
  • 12 SSD в RAID
  • Автовакуум выключен
  • maintenance_work_mem: 64 ГБ (слишком высоко?)

Таблица спецификаций:

  • Размер: 1,26 ТБ
  • Количество рядов: 10,537 млрд
  • Типичный размер индекса: 3,2 ГБ (разница составляет ~. 5 ГБ)

Определение таблицы:

CREATE TABLE run.perception(
id bigint NOT NULL,
run_id bigint NOT NULL,
frame bigint NOT NULL,
by character varying(45) NOT NULL,
by_anyone bigint NOT NULL,
by_me bigint NOT NULL,
by_s_id integer,
owning_p_id bigint NOT NULL,
obj_type_set bigint,
seq integer,
subj_id bigint NOT NULL,
subj_state_frame bigint NOT NULL,
CONSTRAINT perception_pkey PRIMARY KEY (id))

(Не читайте слишком много имен столбцов - я их немного запутал.)

Справочная информация:

  • У нас есть отдельная команда, которая использует эти данные, но на самом деле есть только один или два пользователя. (Все эти данные генерируются с помощью симуляции.) Пользователи начинают анализировать данные только после того, как вставки завершены и индексы полностью построены. Наша главная задача - сократить время, необходимое для генерации полезных данных, и сейчас узким местом является время создания индекса.
  • Скорость запросов была полностью адекватной при использовании партиалов. На самом деле, я думаю, что мы могли бы увеличить количество прогонов, охватываемых каждым индексом, и при этом поддерживать достаточную производительность запросов.
  • Я предполагаю, что нам придется разделить таблицу. Мы пытаемся исчерпать все другие варианты, прежде чем идти по этому маршруту.
burnsy
источник
Эта дополнительная информация будет полезна: типы данных участвующих столбцов, типичный запрос, количество элементов (количество строк), сколько различно run_id? Равномерно распределены? Размер результирующего индекса на диске? Данные статичны, хорошо. Но ты единственный пользователь?
Эрвин Брандштеттер
Обновлено с дополнительной информацией.
Burnsy
1
« Автовакуум выключен » - почему? Это действительно плохая идея. Это предотвращает сбор статистики и, следовательно, приведет к неправильным планам запросов
a_horse_with_no_name
@a_horse_with_no_name Мы вручную запускаем анализ после того, как все данные вставлены
burnny
Ваша ситуация все еще неясна для меня. Как выглядят ваши запросы? Если ваш стол completely static, то что вы имеете в виду We have a separate team onsite that consumes this data? Вы просто индексируете диапазон run_id >= 266 AND run_id <= 270или всю таблицу? Какова продолжительность жизни каждого индекса / сколько запросов будет его использовать? Сколько разных значений для run_id? Звучит как ~ 15 млн. строк за run_id, что сделает около 800 различных значений для run_id? Почему obj_type_set, by_s_id, seqне определены как NOT NULL? Какой приблизительный процент значений NULL для каждого?
Эрвин Брандстеттер

Ответы:

8

BRIN index

Доступно с Postgres 9.5 и, вероятно, именно то, что вы ищете. Гораздо быстрее создание индекса, гораздо меньший индекс. Но запросы обычно не такие быстрые. Руководство:

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

Читайте дальше, это еще не все.
Депес провел предварительный тест.

Оптимальный для вашего случая: Если вы можете писать строки кластерных на run_id, ваш индекс становится очень малым и созданием гораздо дешевле.

CREATE INDEX foo ON run.perception USING brin (run_id, frame)
WHERE run_id >= 266 AND run_id <= 270;

Вы можете даже просто проиндексировать всю таблицу .

Макет таблицы

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

CREATE TABLE run.perception(
  id               bigint NOT NULL PRIMARY KEY
, run_id           bigint NOT NULL
, frame            bigint NOT NULL
, by_anyone        bigint NOT NULL
, by_me            bigint NOT NULL
, owning_p_id      bigint NOT NULL
, subj_id          bigint NOT NULL
, subj_state_frame bigint NOT NULL
, obj_type_set     bigint
, by_s_id          integer
, seq              integer
, by               varchar(45) NOT NULL -- or just use type text
);

Уменьшает размер таблицы на 79 ГБ, если ни один из столбцов не имеет значений NULL. Подробности:

Кроме того, у вас есть только три столбца, которые могут быть NULL. Растровое изображение NULL занимает 8 байтов для 9 - 72 столбцов. Если только один целочисленный столбец имеет значение NULL, то для парадокса хранения есть угол: было бы дешевле использовать вместо этого фиктивное значение: 4 байта потрачено впустую, а 8 байтов сохранено, поскольку для строки не требуется битовая карта NULL. Подробнее здесь:

Частичные индексы

В зависимости от ваших реальных запросов может оказаться более эффективным иметь эти пять частичных индексов вместо приведенного выше:

CREATE INDEX perception_run_id266_idx ON run.perception(frame) WHERE run_id = 266;
CREATE INDEX perception_run_id266_idx ON run.perception(frame) WHERE run_id = 267;
CREATE INDEX perception_run_id266_idx ON run.perception(frame) WHERE run_id = 268;
CREATE INDEX perception_run_id266_idx ON run.perception(frame) WHERE run_id = 269;
CREATE INDEX perception_run_id266_idx ON run.perception(frame) WHERE run_id = 270;

Запустите одну транзакцию для каждой.

run_idТаким образом, удаление в качестве столбца индекса экономит 8 байтов на каждую запись индекса - 32 вместо 40 байтов на строку. Каждый индекс также дешевле создавать, но создание пяти вместо одного занимает значительно больше времени для таблицы, которая слишком велика для того, чтобы оставаться в кэше (как прокомментировали @ Jürgen и @Chris). Так что это может или не может быть полезным для вас.

Разметка

На основе наследования - единственный вариант до Postgres 9.5.
(Новое декларативное разбиение в Postgres 11 или, предпочтительно, 12 умнее.)

Руководство:

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

Жирный акцент мой. Следовательно, оценивая 1000 различных значений run_id, вы создадите разделы, охватывающие около 10 значений каждый.


maintenance_work_mem

Я пропустил, что вы уже корректируете maintenance_work_memв моем первом чтении. Я оставлю цитату и совет в своем ответе для справки. По документации:

maintenance_work_mem (Целое число)

Указывает максимальный объем памяти , который будет использоваться операций по техническому обслуживанию, таких как VACUUM, CREATE INDEXи ALTER TABLE ADD FOREIGN KEY. По умолчанию это 64 мегабайта ( 64MB). Поскольку только одна из этих операций может быть выполнена за один раз сеансом базы данных, и у установки обычно нет многих из них, запущенных одновременно, безопасно установить это значение значительно больше, чем work_mem. Большие настройки могут повысить производительность для очистки и восстановления дампов базы данных.

Обратите внимание, что при autovacuumзапуске autovacuum_max_workersможет быть выделено время для этой памяти, поэтому будьте осторожны, чтобы не установить слишком высокое значение по умолчанию. Может быть полезно контролировать это отдельно setting autovacuum_work_mem.

Я бы установил его настолько высоко, насколько это необходимо - что зависит от неизвестного (для нас) размера индекса. И только локально для выполнения сеанса. Как объясняет цитата, в противном случае слишком высокая общая настройка может привести к неэффективной работе сервера, поскольку автоочистка также может потребовать больше оперативной памяти. Кроме того, не устанавливайте его намного выше необходимого, даже во время выполнения сеанса свободная память может быть использована для кэширования данных.

Это может выглядеть так:

BEGIN;

SET LOCAL maintenance_work_mem = 10GB;  -- depends on resulting index size

CREATE INDEX perception_run_frame_idx_run_266_thru_270 ON run.perception(run_id, frame)
WHERE run_id >= 266 AND run_id <= 270;

COMMIT;

О себе SET LOCAL:

Эффекты SET LOCALдействуют только до конца текущей транзакции, независимо от того, зафиксированы они или нет.

Чтобы измерить размеры объекта:

Сервер обычно должен быть настроен разумно иначе, очевидно.

Эрвин Брандштеттер
источник
Бьюсь об заклад, его работа связана с IO, поскольку таблица намного больше, чем RAM. Чтение таблицы еще чаще усугубит проблему, независимо от того, достаточно ли памяти для сортировки каждого созданного индекса в памяти или нет.
Юрген Штробель
Я с Юргеном в этом. Я считаю, что из-за размера таблицы, по сути, вы должны выполнить полное последовательное сканирование таблицы для каждого созданного индекса. Кроме того, я не уверен, что вы увидите значительное увеличение производительности от создания отдельных частичных индексов (я на 90% уверен, что вы не увидите никакого увеличения, но в этом я мог бы быть отключен.) Я считаю, что лучше Решение для создания индекса будет включать создание одного индекса по всему диапазону, который вы хотите запросить как «отдельный частичный индекс», чтобы сократить общее время сборки.
Крис
@Chris: я согласен, создание 5 индексов займет больше времени, чем один (даже если все они вместе меньше, создание каждого индекса дешевле и запросы могут быть быстрее). Подумав об этом еще немного, это должен быть идеальный вариант использования индекса BRIN в Postgres 9.5.
Эрвин Брандштеттер,
3

Может быть, это просто чрезмерно спроектировано. Вы на самом деле пытались использовать один полный индекс? Частичные индексы, охватывающие всю таблицу вместе, не дают большого выигрыша, если таковые имеются, для поиска индекса, и из вашего текста я делаю вывод, что у вас есть индексы для всех run_ids? Могут быть некоторые преимущества при сканировании индекса с частичными индексами, но я бы в первую очередь проверил простое одноиндексное решение.

Для каждого создания индекса необходимо полное сканирование IO через таблицу. Таким образом, создание нескольких частичных индексов требует гораздо больше операций ввода-вывода при чтении таблицы, чем для одного индекса, хотя сортировка будет выполняться на диске для одного большого индекса. Если вы настаиваете на частичных индексах, вы можете попытаться построить все (или несколько) индексов одновременно (если позволяет память).

Для приблизительной оценки обслуживания_work_mem, необходимого для сортировки всех run_ids, представляющих собой 8-байтовые бигинты, в памяти вам потребуется 10,5 * 8 ГБ + некоторые накладные расходы.

Юрген Штробель
источник
0

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

Вы также можете рассмотреть возможность разделения таблицы по тем же критериям, что и ваши частичные индексы. Это позволило бы при запросе использовать ту же скорость, что и индекс, без создания какого-либо индекса вообще.

Кирк Ройбал
источник