ХабрПостыНовостиМегапосты?

Глобальные индексы для секций в Postgres Pro: глобальная уникальность без костылей

Секционирование таблиц — мощный инструмент PostgreSQL для управления очень большими таблицами с помощью разбиения их на более мелкие части — секции. Основное преимущество достигается, когда запросы содержат условия по ключу секционирования, позволяя СУБД сканировать только необходимые секции (partition pruning), что может кардинально увеличить скорость таких запросов. Кроме того, секционирование упрощает некоторые задачи обслуживания, например быстрое удаление устаревших данных удалением целой секции.

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

Локальные индексы создаются для каждой секции таблицы отдельно. Их плюсы — простота реализации и скорость операций внутри отдельной секции. Но что делать, если нужно обеспечить уникальность значения какого‑то поля во всей секционированной таблице, а не только в одной секции? Или когда требуется выполнить поиск данных, охватывающий сразу несколько секций? Здесь возможности локальных индексов ограничены.

В чём проблема локальных индексов для секционированных таблиц?

  1. Ограничение уникальности. Стандартный PostgreSQL позволяет создать уникальный индекс (или ограничение UNIQUE/PRIMARY KEY) для секционированной таблицы, только если этот индекс включает все столбцы ключа секционирования. Если вам нужна уникальность по полю, которое не является частью ключа секционирования (например, email пользователя в таблице, секционированной по дате регистрации), стандартными средствами этого добиться нельзя. Локальные индексы здесь бессильны, так как гарантируют уникальность лишь в пределах своей секции.

  2. Производительность запросов по нескольким секциям. Каждый локальный индекс «знает» только о своей секции. Когда запрос затрагивает данные из разных секций, например поиск всех заказов клиента за год в таблице, секционированной по месяцам, PostgreSQL приходится последовательно обращаться к локальным индексам каждой релевантной секции. При множестве секций это может быть неэффективно.

Чтобы решить эти проблемы, в Postgres Pro Enterprise появились глобальные индексы. Они работают на уровне всей секционированной таблицы, обеспечивая как глобальную уникальность, так и оптимизацию запросов, охватывающих несколько секций.

Как работают глобальные индексы?

В отличие от локальных индексов, «привязанных» к каждой секции, глобальный индекс создаётся один раз для родительской секционированной таблицы и охватывает данные всех её секций. Физически он содержит данные индексируемых столбцов из всех секций.

Поиск с помощью глобального индекса Postgres Pro осуществляется в два этапа:

  1. Поиск в глобальном индексе. Когда выполняется запрос к секционированной таблице, Postgres Pro сначала обращается к глобальному индексу. Индекс строится по тем столбцам, которые вы указали при его создании. Важный нюанс: в индекс автоматически добавляются столбцы первичного ключа секционированной таблицы в качестве неключевых (аналогично INCLUDE). Эти столбцы хранятся в индексе, но не участвуют в первичном поиске по дереву индекса.

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

Технические детали реализации

-- Вначале устанавливаем расширение
CREATE EXTENSION pgpro_gbtree;

-- Пример создания уникального глобального индекса на столбец email
-- (предполагается, что у таблицы my_partitioned_table есть PRIMARY KEY)
CREATE UNIQUE INDEX my_global_unique_index ON my_partitioned_table USING gbtree (email);

Производительность

Тестирование показывает следующие тенденции:

Операции и обслуживание

Текущие ограничения 

Глобальные индексы выпущены в составе Postgres Pro Enterprise 17.5.1 как экспериментальная функциональность. Существуют следующие ограничения:

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

Применение на практике

Несмотря на ограничения, глобальные индексы уже сейчас могут быть незаменимы:

  1. Обеспечение глобальной уникальности. Основной сценарий: нужно гарантировать уникальность значения (например, email, номер_договора) по всей секционированной таблице, а не только внутри секции. Глобальный индекс UNIQUE решает эту задачу. Это критично для финансовых систем, CRM, систем учёта.

  2. Ускорение запросов по несекционирующим ключам. Если часты запросы с фильтрацией по столбцу, не входящему в ключ секционирования, и эти запросы затрагивают много секций (например, поиск всех заказов клиента по client_id в таблице orders, секционированной по дате), глобальный индекс по client_id может радикально ускорить такие выборки.

  3. Упрощение разработки. Разработчику не нужно учитывать структуру секций при написании запросов, в которых фильтрация или поиск выполняются по столбцам с глобальным индексом, — таблица воспринимается как единое целое.

Заключение

Глобальные индексы — важное дополнение к возможностям секционирования в Postgres Pro Enterprise. Они решают давние проблемы с обеспечением глобальной уникальности и производительностью запросов по несекционирующим ключам в больших секционированных таблицах. Текущая реализация является экспериментальной и имеет ряд ограничений, но уже сейчас предоставляет мощный инструмент для определённых сценариев. Мы ожидаем обратную связь от пользователей для дальнейшего развития и улучшения этой функциональности.

@slonik_pg
04.09.2025 16:08 UTC
Первоисточник

Комментарии

@unfilled
05.09.2025 07:13 UTC
0

Спасибо за статью.

А можете прояснить - как работает DETACH | DELETE PARTITION? Как условный delete "из индекса" - т.е. для каждой строки в отсоединяемой партиции нужно будет найти и отметить строку в глобальном индексе? Или же записи остаются в индексе условно "живыми", а удалёнными отмечаются только при попытке доступа к ним?

05.09.2025 12:21 UTC
+2

>как работает DETACH | DELETE PARTITION?

При операциях удаления и DETACH PARTITION глобальный индекс не перестраивается. То есть, для глобального индекса является нормальным содержать некоторое количество недействительных данных (которые могут быть очищены с помощью команды VACUUM).

>Или же записи остаются в индексе условно "живыми", а удалёнными отмечаются только при попытке доступа к ним?

В глобальном индексе записи не помечаются как удалённые, так как в глобальном индексе хранятся не ссылки на версии записей в heap-таблицах (как в btree), а значения ключевых полей. Поэтому помечать как удалёнными записи не получится, так как при повторном добавлении записи с теми же значениями индексную запись придётся включать.

05.09.2025 13:22 UTC
0

Интересно, спасибо.

@karpova_ip
11.09.2025 09:16 UTC
0

Спасибо за статью.

Есть вопрос: говоря о секционированных таблицах, вы имеете в виду таблицы, хранящиеся на одном сервере? То есть без использования шардирования в рамках распределенной базы данных?

11.09.2025 09:16 UTC
0

Да, всё верно, имеются в виду таблицы, хранящиеся на одном сервере.

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

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

11.09.2025 11:45 UTC
0

Спасибо, теперь ясно.

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