F.30. pg_pathman
pg_pathman
— это расширение Postgres Pro, реализующее оптимизированное решение для секционирования больших и распределённых баз данных. Используя pg_pathman
, вы можете:
Секционировать большие базы данных, не прерывая их работу.
Ускорять выполнение запросов с секционированными таблицами.
Управлять существующими и добавлять новые секции на лету.
Добавлять в качестве секций сторонние таблицы.
Соединять секционированные таблицы для операций чтения и записи.
Это расширение совместимо с Postgres Pro 9.5 и новее.
F.30.1. Установка и подготовка
Расширение pg_pathman
включено в состав Postgres Pro. Установив Postgres Pro, выполните следующие действия, чтобы подготовить pg_pathman
к работе:
Добавьте
pg_pathman
в переменнуюshared_preload_libraries
в файлеpostgresql.conf
:shared_preload_libraries = 'pg_pathman'
Важно
pg_pathman
может конфликтовать с другими расширениями, использующими те же функции для перехвата управления. Например, возможен конфликтpg_pathman
сpg_stat_statements
, так как оба эти расширения используют функциюProcessUtility_hook
. Во избежание подобных проблемpg_pathman
должен быть всегда последним в списке библиотек:shared_preload_libraries = 'pg_stat_statements, pg_pathman'
Перезапустите Postgres Pro, чтобы изменения вступили в силу.
Создайте расширение
pg_pathman
, выполнив следующий запрос:CREATE EXTENSION pg_pathman;
После того как расширение pg_pathman
будет создано, вы можете приступить к секционированию таблиц.
Подсказка
Вы также можете скомпилировать pg_pathman
из исходного кода, выполнив следующую команду в каталоге pg_pathman
:
make install USE_PGXS=1
Завершив эту операцию, выполните следующие действия для окончания установки.
Также не забудьте дополнительно установить переменную PG_CONFIG
, если вы хотите испытать pg_pathman
в нестандартной сборке Postgres Pro. Подробнее об этом вы можете прочитать здесь.
Включать/отключать pg_pathman
или его определённые узлы можно с помощью переменных GUC. За подробностями обратитесь к Подразделу F.30.5.1.
Если вы хотите полностью отключить pg_pathman
для ранее секционированной таблицы, воспользуйтесь функцией disable_pathman_for()
:
SELECT disable_pathman_for('range_rel');
Все секции и данные останутся неизменными и будут обрабатываться стандартным механизмом наследования Postgres Pro.
F.30.1.1. Обновление расширения pg_pathman
Если у вас уже была установлена предыдущая версия pg_pathman
, выполните следующие действия для установки новой версии:
Установите Postgres Pro.
Перезапустите кластер Postgres Pro.
Если у вас уже была установлена предыдущая основная версия
pg_pathman
(в её номере другая вторая цифра), выполните следующие действия для установки новой версии:ALTER EXTENSION pg_pathman UPDATE TO
версия
; SET pg_pathman.enable = t;Здесь
версия
— это номер основной версииpg_pathman
, например, 1.5.Узнать текущую версию
pg_pathman
можно, воспользовавшись функциейpathman_version()
.
F.30.2. Использование
Содержимое
Выбор стратегии секционирования
Осуществление неблокирующего переноса данных
Секционирование по одному выражению
Секционирование по составному ключу
Реализация многоуровневого секционирования
По мере увеличения базы данных механизмы индексирования могут становиться неэффективными и запросы могут выполняться намного медленнее. Для повышения производительности, улучшения масштабируемости и оптимизации процессов администрирования вы можете использовать секционирование — разделение большой таблицы на множество меньших по размеру, когда все строки размещаются в секциях согласно ключу разбиения.
Исторически Postgres Pro поддерживал секционирование через механизм наследования, когда каждая секция создавалась в виде дочерней таблицы с ограничением-проверкой. В Postgres Pro 10 появилась поддержка декларативного секционирования по диапазонам и по спискам, которая также полагается на наследование. При таком подходе планировщик запросов должен выполнить полный перебор и проверку условий ограничений для каждой секции, чтобы построить план выполнения запроса, что влечёт замедление запросов к таблицам с большим количеством секций. Расширение pg_pathman
использует оптимизированные алгоритмы планирования и функции секционирования, учитывающие внутреннюю структуру секционированных таблиц, что позволяет добиться лучшей производительности. Более подробно детали реализации pg_pathman
описаны в Подразделе F.30.4.
F.30.2.1. Выбор стратегии секционирования
Расширение pg_pathman
поддерживает следующие стратегии секционирования:
По хешу — строки сопоставляются с секциями с использованием универсальной функции хеширования. Выберите эту стратегию, если в большинстве запросов будет выполняться поиск точного соответствия.
По диапазонам — строки сопоставляются с секциями по диапазонам разбивающего ключа, назначаемым каждой секции. Выберите эту стратегию, если ваша база данных содержит числовые данные, которые, скорее всего, будут представлять интерес как значения в интервалах. Например, вас могут интересовать исторические данные по годам или результаты экспериментов в определённых числовых диапазонах. Для получения выигрыша в производительности
pg_pathman
использует алгоритм бинарного поиска.
По умолчанию pg_pathman
переносит все данные из родительской таблицы в создаваемые секции сразу (производится блокирующее секционирование). При таком подходе вы можете изменить структуру таблицы в одной транзакции, но если объём данных велик, это может привести к приостановке работы. Если важно, чтобы работа не прерывалась, вы можете выполнить параллельное секционирование. В этом случае pg_pathman
записывает все новые данные в созданные секции, но сохраняет исходные данные в родительской таблице, пока вы явно не перенесёте их. Это позволяет секционировать большие базы данных, не прерывая работу, так как вы можете выбрать удобное время для переноса данных и переносить их небольшими порциями, не блокируя другие транзакции. Подробнее параллельное секционирование описано в Подразделе F.30.2.2.
F.30.2.1.1. Организация секционирования по хешу
Чтобы выполнить секционирование по хешу с применением pg_pathman
, воспользуйтесь функцией create_hash_partitions()
:
create_hash_partitions(parent_relid REGCLASS, expression TEXT, partitions_count INTEGER, partition_data BOOLEAN DEFAULT TRUE, partition_names TEXT[] DEFAULT NULL, tablespaces TEXT[] DEFAULT NULL)
Модуль pg_pathman
создаёт указанное число секций, используя хеш-функцию. Вы можете также указать имена секций и табличных пространств, задав параметры partition_names
и tablespaces
, соответственно.
После того как таблица разделена на секции, удалять или добавлять секции в ней нельзя. Однако при необходимости определённую секцию можно заменить другой таблицей:
replace_hash_partition(old_partition REGCLASS, new_partition REGCLASS, lock_parent BOOL DEFAULT TRUE);
Если параметр lock_parent
равен true
, никакие запросы INSERT
/UPDATE
/ALTER TABLE
в родительской таблице не разрешаются.
Если вы опустите необязательный параметр partition_data
или зададите для него значение true
, все данные из родительской таблицы будут перенесены в секции. Модуль pg_pathman
заблокирует эту таблицу для других транзакций до завершения переноса данных. Чтобы избежать приостановки работы, вы можете передать в параметре partition_data
значение false
и затем вызвать функцию partition_table_concurrently()
для переноса данных без блокирования других запросов. За подробностями обратитесь к Подразделу F.30.2.2.
F.30.2.1.2. Организация секционирования по диапазонам
Модуль pg_pathman
предоставляет функцию create_range_partitions()
для разбиения по диапазонам. Эта функция создаёт секции, исходя из заданного интервала и начального значения ключа разбиения. Новые секции будут создаваться автоматически при добавлении данных, не попадающих в ранее охваченный интервал.
create_range_partitions(parent_relid REGCLASS, expression TEXT, start_value ANYELEMENT, p_interval ANYELEMENT | INTERVAL, p_count INTEGER DEFAULT NULL, partition_data BOOLEAN DEFAULT TRUE)
Модуль pg_pathman
создаёт секции в зависимости от переданных параметров. Если необязательный параметр p_count
опущен, pg_pathman
вычисляет требуемое число секций, исходя из заданного начального значения и интервала. Если затем будут добавляться данные вне ранее заданного диапазона, pg_pathman
создаст новые секции автоматически, сохраняя указанный интервал. При таком подходе все секции оказываются одного размера, что может способствовать ускорению запросов и упрощению управления данными.
Также вы можете задать массив, определяющий границы создаваемых секций, в параметре bounds
:
create_range_partitions(parent_relid REGCLASS, expression TEXT, bounds ANYARRAY, partition_names TEXT[] DEFAULT NULL, tablespaces TEXT[] DEFAULT NULL, partition_data BOOLEAN DEFAULT TRUE)
Если требуется, вы можете также использовать функции управления секциями и добавлять секции вручную. Например, если между созданными секциями образовался промежуток, pg_pathman
не сможет заполнить его новой секцией в автоматическом режиме.
По умолчанию все данные из родительской таблицы будут перенесены в указанное количество секций. Модуль pg_pathman
заблокирует эту таблицу для других транзакций до завершения переноса данных. Чтобы избежать приостановки работы, вы можете передать в параметре partition_data
значение false
и затем вызвать функцию partition_table_concurrently()
для переноса данных без блокирования других запросов. За подробностями обратитесь к Подразделу F.30.2.2.
F.30.2.2. Осуществление неблокирующего переноса данных
Если важно не допустить прерывания работы, вы можете произвести секционирование в параллельном режиме, установив для параметра partition_data
значение false
. В этом случае pg_pathman
создаст пустые секции и оставит все исходные данные в родительской таблице. При этом все новые записи будут попадать в созданные секции. Позднее вы сможете переместить все начальные данные в соответствующие секции, не блокируя другие запросы, воспользовавшись функцией partition_table_concurrently()
:
partition_table_concurrently(relation REGCLASS, batch_size INTEGER DEFAULT 1000, sleep_time FLOAT8 DEFAULT 1.0)
Здесь:
relation
— родительская таблица.batch_size
— количество строк, которое должно копироваться из родительской таблицы в секции за один раз. Этот параметр может принимать любое целое значение от 1 до 10000.sleep_time
— интервал времени между попытками переноса данных, в секундах.
Модуль pg_pathman
запускает фоновый рабочий процесс для переноса данных из родительской таблицы в секции маленькими порциями, размер которых задаётся параметром batch_size
. Если одна или несколько строк в порции оказались заблокированы другими запросами, pg_pathman
ждёт заданное время (sleep_time
) и повторяет попытку (до 60 раз). За процессом переноса данных можно наблюдать в представлении pathman_concurrent_part_tasks
, показывающем количество строк, обработанных на данный момент:
[user]postgres: select * from pathman_concurrent_part_tasks ; userid | pid | dbid | relid | processed | status --------+-------+-------+-------+-----------+--------- user | 20012 | 12413 | test | 334000 | working (1 row)
Если потребуется остановить перенос данных, вы можете в любое время выполнить функцию stop_concurrent_part_task()
:
SELECT stop_concurrent_part_task(relation REGCLASS);
pg_pathman
завершит перенос текущей порции и прекратит процесс переноса.
Подсказка
Когда pg_pathman
перенесёт все данные из родительской таблицы, вы можете исключить её из плана запроса. За подробностями обратитесь к описанию функции set_enable_parent()
.
F.30.2.3. Секционирование по одному выражению
Для стратегий секционирования по диапазону и хешу pg_pathman
поддерживает разбиение по выражению, возвращающему одно скалярное значение. Выражением разбиения может быть ссылка на столбец таблицы или вычисление ключа разбиения с использованием значений одного или нескольких столбцов.
Подсказка
Если вас интересует разбиение таблицы по значению кортежа, обратитесь к Подразделу F.30.2.4.
Для разбиения таблицы по выражению используйте функции секционирования pg_pathman
. Выражение разбиения должно соответствовать следующим условиям:
В выражении должен фигурировать минимум один столбец секционируемой таблицы.
Все фигурирующие в нём столбцы должны иметь свойство
NOT NULL
.В выражении нельзя обращаться к системным атрибутам, таким как
oid
,xmin
,xmax
и т. д.В выражение нельзя включать подзапросы.
Все функции, используемые выражением, должны быть помечены как
IMMUTABLE
.
Так как выражение может возвращать значение практически любого типа, возвращаемое значение нужно привести к типу, для которого выполняется разбиение.
Для обращения к секции необходимо использовать в точности то выражение, по которому выполнено разбиение. В противном случае pg_pathman
не сможет оптимизировать запрос. Просмотреть выражения разбиения для всех секционированных таблиц можно в таблице pathman_config
.
F.30.2.3.1. Примеры
Предположим, что у вас есть таблица test
, содержащая некоторые данные jsonb
:
CREATE TABLE test(col jsonb not null); INSERT INTO test SELECT format('{"key": %s, "date": "%s", "value": "%s"}', i, current_date, md5(i::text))::jsonb FROM generate_series(1, 10000 * 10) as g(i);
Для разбиения этой таблицы по диапазонам значения key
, вам нужно извлечь это значение из объекта jsonb
и преобразовать его в числовой тип, например, в bigint
:
SELECT create_range_partitions('test', '(col->>''key'')::bigint', 1, 10000, 10);
В результате pg_pathman
разделит родительскую таблицу на десять секций и поместит в каждую 10000 строк:
SELECT * FROM pathman_partition_list; parent | partition | parttype | expr | range_min | range_max --------+-----------+----------+---------------------------------+-----------+----------- test | test_1 | 2 | ((col ->> 'key'::text))::bigint | 1 | 10001 test | test_2 | 2 | ((col ->> 'key'::text))::bigint | 10001 | 20001 test | test_3 | 2 | ((col ->> 'key'::text))::bigint | 20001 | 30001 test | test_4 | 2 | ((col ->> 'key'::text))::bigint | 30001 | 40001 test | test_5 | 2 | ((col ->> 'key'::text))::bigint | 40001 | 50001 test | test_6 | 2 | ((col ->> 'key'::text))::bigint | 50001 | 60001 test | test_7 | 2 | ((col ->> 'key'::text))::bigint | 60001 | 70001 test | test_8 | 2 | ((col ->> 'key'::text))::bigint | 70001 | 80001 test | test_9 | 2 | ((col ->> 'key'::text))::bigint | 80001 | 90001 test | test_10 | 2 | ((col ->> 'key'::text))::bigint | 90001 | 100001 (10 rows)
F.30.2.4. Секционирование по составному ключу
Используя pg_pathman
, вы также можете реализовать диапазонное секционирование по составному ключу. Составной ключ образуется из двух или нескольких разделённых запятыми элементов, которыми могут быть ссылки на столбцы или выражения, извлекающие значения из таблицы. Выражения, определяющие составной ключ, должны удовлетворять условиям, перечисленным в Подразделе F.30.2.3.
Хотя pg_pathman
не поддерживает автоматическое создание секций с составным ключом, вы можете добавлять секции, используя функцию add_range_partition()
. Обычно это происходит так:
Включите автоматическое наименование секций для вашей таблицы, вызвав функцию
create_naming_sequence()
.Создайте составной ключ разбиения.
Зарегистрируйте таблицу, которую вы будете секционировать с помощью
pg_pathman
, воспользовавшись функциейadd_to_pathman_config()
.Добавьте секции на основе составного ключа разбиения, вызвав функцию
add_range_partition()
.
F.30.2.4.1. Примеры
Предположим, что у вас есть таблица test
, содержащая некоторые данные с датами:
CREATE TABLE test (logdate date not null, comment text);
Для разбиения таких данных по месяцам и годам вам нужно создать составной ключ:
CREATE TYPE test_key AS (year float8, month float8);
Для включения автоматического именования секций выполните функцию create_naming_sequence()
, передав в качестве аргумента имя функции:
SELECT create_naming_sequence('test');
Зарегистрируйте таблицу test
в pg_pathman
, указав ключ разбиения, который вы намерены использовать:
SELECT add_to_pathman_config('test', '( extract(year from logdate), extract(month from logdate) )::test_key', NULL);
Создайте секцию, включающую все данные в интервале десяти лет, начиная с января текущего кода:
SELECT add_range_partition('test', (extract(year from current_date), 1)::test_key, (extract(year from current_date + '10 years'::interval), 1)::test_key);
F.30.2.5. Реализация многоуровневого секционирования
pg_pathman
поддерживает многоуровневое секционирование для стратегий разбиения и по хешу, и по диапазонам. Вы можете использовать эти стратегии в любом сочетании: таблица, секционирования по хешам или диапазонам, затем может быть секционирована и по диапазонам, и по хешу.
Чтобы разбить существующую секцию на несколько дочерних, используйте обычные функции секционирования pg_pathman
, как описано в Подразделе F.30.2.1, передавая имя данной секции в параметре parent_relid
. Точные имена секций вы можете узнать из представления pathman_partition_list.
При реализации диапазонного разбиения с внутренним диапазонным разбиением можно выбрать для внутреннего разбиения либо другое выражение, либо то же, что задано для родительской таблицы. В первом случае, если выбранный диапазон больше диапазона родительской таблицы, будут использоваться только те дочерние секции, диапазон которых пересекается с родительским. Другие дочерние секции останутся пустыми, если только не произойдёт объединение их родительской секции с соседними, в результате которого хотя бы частично будет охвачен их диапазон.
F.30.2.5.1. Примеры
Предположим, что у вас есть таблица journal
, секционированная по месяцам:
-- создание пустой таблицы CREATE TABLE journal ( id SERIAL, dt TIMESTAMP NOT NULL, level INTEGER, msg TEXT); -- добавление в таблицу некоторых данных INSERT INTO journal (dt, level, msg) SELECT g, random() * 6, md5(g::text) FROM generate_series('2015-01-01'::date, '2015-12-31'::date, '1 minute') as g; -- секционирование таблицы по диапазонам SELECT create_range_partitions('journal', 'dt', '2015-01-01'::date, '1 month'::interval);
Если в какой-то момент становится выгоднее иметь меньшие секции, вы можете дополнительно разбить существующие по диапазонам или по секциям. Например, чтобы разбить секцию journal_1
на меньшие по дням, выполните:
SELECT create_range_partitions('journal_1', 'dt', '2015-01-01'::date, '1 day'::interval);
Подобным образом вы можете создать вложенные секции с разбиением по хешу. Например, так можно разбить секцию journal_2
на пять секций, используя столбец id
в качестве ключа разбиения:
SELECT create_hash_partitions('journal_2', 'id', '5');
F.30.2.6. Управление секциями
pg_pathman
предоставляет набор функций для простого управления секциями. За подробностями обратитесь к Подразделу F.30.5.3.4.
F.30.3. Примеры
F.30.3.1. Общие рекомендации
Так можно получить столбец
partition
, содержащий имена нижележащих секций, воспользовавшись системным атрибутомtableoid
:SELECT tableoid::regclass AS partition, * FROM partitioned_table;
Несмотря на то, что индексы в родительской таблице не очень полезны (так как предполагается, что она пуста), они выполняют роль прототипов для создания индексов в секциях. Для каждого индекса в родительской таблице
pg_pathman
создаёт подобный индекс в каждой секции.Получить список всех текущих задач параллельного разбиения можно в представлении
pathman_concurrent_part_tasks
:SELECT * FROM pathman_concurrent_part_tasks; userid | pid | dbid | relid | processed | status --------+------+-------+-------+-----------+--------- user | 7367 | 16384 | test | 472000 | working (1 row)
Представление
pathman_partition_list
в сочетании сdrop_range_partition()
может использоваться для удаления диапазонных секций более гибким образом по сравнению с обычнымDROP TABLE
:SELECT drop_range_partition(partition, false) /* перенос данных в родительскую таблицу */ FROM pathman_partition_list WHERE parent = 'part_test'::regclass AND range_min::int < 500; NOTICE: 1 rows copied from part_test_11 NOTICE: 100 rows copied from part_test_1 NOTICE: 100 rows copied from part_test_2 drop_range_partition ---------------------- dummy_test_11 dummy_test_1 dummy_test_2 (3 rows)
Вы можете сделать сторонние таблицы секциями с помощью функции
attach_range_partition()
. В результате строки, добавляемые в родительскую таблицу, будут перенаправляться в эти сторонние секции механизмомPartitionFilter
. По умолчанию добавление строк в такие секции разрешается только при использовании обёртки сторонних данныхpostgres_fdw
. Это поведение управляется переменнойpg_pathman.insert_into_fdw
. Для её изменения необходимо иметь права суперпользователя.
F.30.3.2. Секционирование по хешу
Рассмотрим пример секционирования таблицы по хешу. Для начала создадим таблицу с целочисленным столбцом:
CREATE TABLE items ( id SERIAL PRIMARY KEY, name TEXT, code BIGINT); INSERT INTO items (id, name, code) SELECT g, md5(g::text), random() * 100000 FROM generate_series(1, 100000) as g;
Теперь выполним функцию create_hash_partitions()
с подходящими аргументами:
SELECT create_hash_partitions('items', 'id', 100);
Этот запрос создаст новые секции и переместит в них данные из родительской таблицы.
Пример запроса, выполняющего фильтрацию по ключу разбиения:
SELECT * FROM items WHERE id = 1234; id | name | code ------+----------------------------------+------ 1234 | 81dc9bdb52d04dc20036dbd8313ed055 | 1855 (1 row) EXPLAIN SELECT * FROM items WHERE id = 1234; QUERY PLAN ------------------------------------------------------------------------------------ Append (cost=0.28..8.29 rows=0 width=0) -> Index Scan using items_34_pkey on items_34 (cost=0.28..8.29 rows=0 width=0) Index Cond: (id = 1234)
Заметьте, узел Append
содержит только одно дочернее сканирование, соответствующее предложению WHERE
.
Важно
Обратите внимание на тот факт, что pg_pathman
исключает родительскую таблицу из плана запроса.
Чтобы обратиться к родительской таблице, используйте модификатор ONLY
:
EXPLAIN SELECT * FROM ONLY items; QUERY PLAN ------------------------------------------------------ Seq Scan on items (cost=0.00..0.00 rows=1 width=45)
F.30.3.3. Секционирование по диапазонам
Рассмотрим пример секционирования по диапазонам. Давайте создадим таблицу, содержащую сообщения журнала:
CREATE TABLE journal ( id SERIAL, dt TIMESTAMP NOT NULL, level INTEGER, msg TEXT); -- подобный индекс будет также создан для каждой секции CREATE INDEX ON journal(dt); -- генерируются некоторые данные INSERT INTO journal (dt, level, msg) SELECT g, random() * 6, md5(g::text) FROM generate_series('2015-01-01'::date, '2015-12-31'::date, '1 minute') as g;
Выполним функцию create_range_partitions()
, чтобы создать секции, которые будут содержать данные за один день:
SELECT create_range_partitions('journal', 'dt', '2015-01-01'::date, '1 day'::interval);
Этот запрос создаст 364 секции и переместит в них данные из родительской таблицы.
Новые секции добавляются автоматически триггером INSERT, но это можно сделать и вручную, с помощью следующих функций:
-- добавление новой секции с заданным диапазоном SELECT add_range_partition('journal', '2016-01-01'::date, '2016-01-07'::date); -- добавление новой секции с диапазоном по умолчанию SELECT append_range_partition('journal');
Первая создаёт новую секцию с заданным диапазоном. Вторая создаёт новую секцию с диапазоном, заданным при первоначальном разбиении, и добавляет её в конец списка секций. Также можно присоединить существующую таблицу в качестве секции. Например, мы можем подключить таблицу архива (это может быть даже сторонняя таблица с другого сервера) с историческими данными:
CREATE FOREIGN TABLE journal_archive ( id INTEGER NOT NULL, dt TIMESTAMP NOT NULL, level INTEGER, msg TEXT) SERVER archive_server; SELECT attach_range_partition('journal', 'journal_archive', '2014-01-01'::date, '2015-01-01'::date);
Важно
Присоединённая таблица должна содержать такие же столбцы, что и секционируемая, за исключением удалённых. Эти столбцы должны иметь те же типы, правила сортировки и характеристики NOT NULL, что и исходные.
Для слияния двух соседних секций используйте функцию merge_range_partitions()
:
SELECT merge_range_partitions('journal_archive', 'journal_1');
Чтобы разделить секцию по значению, воспользуйтесь функцией split_range_partition()
:
SELECT split_range_partition('journal_366', '2016-01-03'::date);
Чтобы отсоединить секцию, воспользуйтесь функцией detach_range_partition()
:
SELECT detach_range_partition('journal_archive');
Пример запроса, выполняющего фильтрацию по ключу разбиения:
SELECT * FROM journal WHERE dt >= '2015-06-01' AND dt < '2015-06-03'; id | dt | level | msg --------+---------------------+-------+---------------------------------- 217441 | 2015-06-01 00:00:00 | 2 | 15053892d993ce19f580a128f87e3dbf 217442 | 2015-06-01 00:01:00 | 1 | 3a7c46f18a952d62ce5418ac2056010c 217443 | 2015-06-01 00:02:00 | 0 | 92c8de8f82faf0b139a3d99f2792311d ... (2880 rows) EXPLAIN SELECT * FROM journal WHERE dt >= '2015-06-01' AND dt < '2015-06-03'; QUERY PLAN ------------------------------------------------------------------ Append (cost=0.00..58.80 rows=0 width=0) -> Seq Scan on journal_152 (cost=0.00..29.40 rows=0 width=0) -> Seq Scan on journal_153 (cost=0.00..29.40 rows=0 width=0) (3 rows)
F.30.4. Внутреннее устройство
Расширение pg_pathman
сохраняет конфигурацию разбиения в таблице pathman_config
; каждая её строка содержит запись для одной секционированной таблицы (название отношения, столбец разбиения и тип секционирования). На этапе инициализации модуль pg_pathman
кеширует некоторую информацию дочерних секций в общей памяти, а затем она может использоваться при построении плана. Когда начинает выполняться запрос SELECT
, pg_pathman
проходит по дереву условий в поиске выражений вида:
VARIABLE OP CONST
где VARIABLE
— это ключ разбиения, OP
— оператор сравнения (поддерживаются =, <, <=, >, >=), CONST
— скалярное значение. Например:
WHERE id = 150
Затем, учитывая стратегию секционирования и оператор условия, pg_pathman
ищет соответствующие секции и строит план.
F.30.4.1. Дополнительные узлы плана
pg_pathman
предоставляет несколько нестандартных узлов плана, позволяющих сократить время выполнения, а именно:
RuntimeAppend
(переопределяет узел планаAppend
)RuntimeMergeAppend
(переопределяет узел планаMergeAppend
)PartitionFilter
(выполняет роль триггеров INSERT)PartitionRouter
реализует межсекционные UPDATE вместо триггеров
PartitionFilter
действует как прокси-узел для дочерних узлов INSERT, то есть он может перенаправлять выходные кортежи в соответствующие секции:
EXPLAIN (COSTS OFF) INSERT INTO partitioned_table SELECT generate_series(1, 10), random(); QUERY PLAN ----------------------------------------- Insert on partitioned_table -> Custom Scan (PartitionFilter) -> Subquery Scan on "*SELECT*" -> Result (4 rows)
Узел PartitionRouter
представляет собой ещё один промежуточный узел, используемый вместе с PartitionFilter
для выполнения межсекционных операций UPDATE
, имеющих место, например, при изменении одного из столбцов ключа разбиения.
Важно
Узел PartitionRouter
преобразует межсекционные команды UPDATE
в DELETE
+ INSERT
. В Postgres Pro до 11 версии эта операция небезопасна, так как pg_pathman
не имеет возможности определить, была ли изменённая строка удалена или перемещена в другую секцию.
По умолчанию узел PartitionRouter
отключён во избежание нежелательных побочных эффектов. Чтобы включить его, задайте для параметра pg_pathman.enable_partitionrouter
значение on
.
EXPLAIN (COSTS OFF) UPDATE partitioned_table SET value = value + 1 WHERE value = 2; QUERY PLAN --------------------------------------------------- Update on partitioned_table_0 -> Custom Scan (PartitionRouter) -> Custom Scan (PartitionFilter) -> Seq Scan on partitioned_table_0 Filter: (value = 2) (5 rows)
RuntimeAppend
и RuntimeMergeAppend
имеют много общего: они оказываются полезными, когда условие WHERE принимает вид:
VARIABLE OP PARAM
Подобные выражения уже не могут быть оптимизированы во время планирования, так как значение параметра оказывается неизвестным до стадии выполнения. Решить эту проблему можно, включив процедуру анализа условия WHERE в первоначальный код узла Append
, таким образом позволяя ему выбирать только нужные варианты сканирования из всего набора планируемых сканирований. Это по сути сводится к созданию нестандартного узла, способного выполнять такие проверки.
Есть по меньше мере несколько ситуаций, которые демонстрируют полезность таких узлов:
/* создать таблицу, которую мы будем секционировать */ CREATE TABLE partitioned_table(id INT NOT NULL, payload REAL); /* вставить произвольные данные */ INSERT INTO partitioned_table SELECT generate_series(1, 1000), random(); /* выполнить секционирование */ SELECT create_hash_partitions('partitioned_table', 'id', 100); /* создать обычную таблицу */ CREATE TABLE some_table AS SELECT generate_series(1, 100) AS VAL;
id = (select ... limit 1)
EXPLAIN (COSTS OFF, ANALYZE) SELECT * FROM partitioned_table WHERE id = (SELECT * FROM some_table LIMIT 1); QUERY PLAN ---------------------------------------------------------------------------------------------------- Custom Scan (RuntimeAppend) (actual time=0.030..0.033 rows=1 loops=1) InitPlan 1 (returns $0) -> Limit (actual time=0.011..0.011 rows=1 loops=1) -> Seq Scan on some_table (actual time=0.010..0.010 rows=1 loops=1) -> Seq Scan on partitioned_table_70 partitioned_table (actual time=0.004..0.006 rows=1 loops=1) Filter: (id = $0) Rows Removed by Filter: 9 Planning time: 1.131 ms Execution time: 0.075 ms (9 rows) /* отключить узел RuntimeAppend */ SET pg_pathman.enable_runtimeappend = f; EXPLAIN (COSTS OFF, ANALYZE) SELECT * FROM partitioned_table WHERE id = (SELECT * FROM some_table LIMIT 1); QUERY PLAN ---------------------------------------------------------------------------------- Append (actual time=0.196..0.274 rows=1 loops=1) InitPlan 1 (returns $0) -> Limit (actual time=0.005..0.005 rows=1 loops=1) -> Seq Scan on some_table (actual time=0.003..0.003 rows=1 loops=1) -> Seq Scan on partitioned_table_0 (actual time=0.014..0.014 rows=0 loops=1) Filter: (id = $0) Rows Removed by Filter: 6 -> Seq Scan on partitioned_table_1 (actual time=0.003..0.003 rows=0 loops=1) Filter: (id = $0) Rows Removed by Filter: 5 ... /* more plans follow */ Planning time: 1.140 ms Execution time: 0.855 ms (306 rows)
id = ANY (select ...)
EXPLAIN (COSTS OFF, ANALYZE) SELECT * FROM partitioned_table WHERE id = any (SELECT * FROM some_table limit 4); QUERY PLAN ----------------------------------------------------------------------------------------------------------- Nested Loop (actual time=0.025..0.060 rows=4 loops=1) -> Limit (actual time=0.009..0.011 rows=4 loops=1) -> Seq Scan on some_table (actual time=0.008..0.010 rows=4 loops=1) -> Custom Scan (RuntimeAppend) (actual time=0.002..0.004 rows=1 loops=4) -> Seq Scan on partitioned_table_70 partitioned_table (actual time=0.001..0.001 rows=10 loops=1) -> Seq Scan on partitioned_table_26 partitioned_table (actual time=0.002..0.003 rows=9 loops=1) -> Seq Scan on partitioned_table_27 partitioned_table (actual time=0.001..0.002 rows=20 loops=1) -> Seq Scan on partitioned_table_63 partitioned_table (actual time=0.001..0.002 rows=9 loops=1) Planning time: 0.771 ms Execution time: 0.101 ms (10 rows) /* отключить узел RuntimeAppend */ SET pg_pathman.enable_runtimeappend = f; EXPLAIN (COSTS OFF, ANALYZE) SELECT * FROM partitioned_table WHERE id = any (SELECT * FROM some_table limit 4); QUERY PLAN ----------------------------------------------------------------------------------------- Nested Loop Semi Join (actual time=0.531..1.526 rows=4 loops=1) Join Filter: (partitioned_table.id = some_table.val) Rows Removed by Join Filter: 3990 -> Append (actual time=0.190..0.470 rows=1000 loops=1) -> Seq Scan on partitioned_table (actual time=0.187..0.187 rows=0 loops=1) -> Seq Scan on partitioned_table_0 (actual time=0.002..0.004 rows=6 loops=1) -> Seq Scan on partitioned_table_1 (actual time=0.001..0.001 rows=5 loops=1) -> Seq Scan on partitioned_table_2 (actual time=0.002..0.004 rows=14 loops=1) ... /* 96 scans follow */ -> Materialize (actual time=0.000..0.000 rows=4 loops=1000) -> Limit (actual time=0.005..0.006 rows=4 loops=1) -> Seq Scan on some_table (actual time=0.003..0.004 rows=4 loops=1) Planning time: 2.169 ms Execution time: 2.059 ms (110 rows)
NestLoop
(вложенный цикл) с секционированной таблицей, которая опущена здесь, так как была показана выше.
Узнать больше о нестандартных узлах плана вы можете в блоге Александра Короткова.
F.30.5. Справка
F.30.5.1. Переменные GUC
Для включения/отключения модуля pg_pathman
и отдельных узлов плана предназначены несколько переменных GUC:
pg_pathman.enable
— включает (отключает) модульpg_pathman
.По умолчанию:
on
(вкл.)pg_pathman.enable_runtimeappend
— включает нестандартный узелRuntimeAppend
.По умолчанию:
on
(вкл.)pg_pathman.enable_runtimemergeappend
— включает нестандартный узелRuntimeMergeAppend
.По умолчанию:
on
(вкл.)pg_pathman.enable_partitionfilter
— включает нестандартный узелPartitionFilter
, выполняющий межсекционные операцииINSERT
.По умолчанию:
on
(вкл.)pg_pathman.enable_partitionrouter
— включает нестандартный узелPartitionRouter
, выполняющий межсекционные операцииUPDATE
.По умолчанию:
off
(выкл.)pg_pathman.enable_auto_partition
— включает автоматическое создание секций (в рамках сеанса).По умолчанию:
on
(вкл.)pg_pathman.enable_bounds_cache
— включает/отключает кеш границ.По умолчанию:
on
(вкл.)pg_pathman.insert_into_fdw
— разрешает использовать операцииINSERT
с различными обёртками сторонних данных. Возможные значения:disabled
(такое использование запрещено),postgres
(разрешено для обёртки postgres) иany_fdw
(разрешено для любых обёрток).По умолчанию:
postgres
pg_pathman.override_copy
— включает/отключает перехват оператораCOPY
.По умолчанию:
on
(вкл.)
F.30.5.2. Представления и таблицы
F.30.5.2.1. pathman_config
В этой таблице хранится список секционированных таблиц. Это основное хранилище конфигурации.
CREATE TABLE IF NOT EXISTS pathman_config ( partrel REGCLASS NOT NULL PRIMARY KEY, attname TEXT NOT NULL, parttype INTEGER NOT NULL, range_interval TEXT);
F.30.5.2.2. pathman_config_params
В этой таблице хранятся дополнительные параметры, переопределяющие стандартное поведение pg_pathman
.
CREATE TABLE IF NOT EXISTS pathman_config_params ( partrel REGCLASS NOT NULL PRIMARY KEY, enable_parent BOOLEAN NOT NULL DEFAULT TRUE, auto BOOLEAN NOT NULL DEFAULT TRUE, init_callback REGPROCEDURE NOT NULL DEFAULT 0, spawn_using_bgw BOOLEAN NOT NULL DEFAULT FALSE);
F.30.5.2.3. pathman_concurrent_part_tasks
В этом представлении показываются все работающие в данный момент задачи параллельного разбиения.
-- вспомогательная функция, возвращающая множество CREATE OR REPLACE FUNCTION show_concurrent_part_tasks() RETURNS TABLE ( userid REGROLE, pid INT, dbid OID, relid REGCLASS, processed INT, status TEXT) AS 'pg_pathman', 'show_concurrent_part_tasks_internal' LANGUAGE C STRICT; CREATE OR REPLACE VIEW pathman_concurrent_part_tasks AS SELECT * FROM show_concurrent_part_tasks();
F.30.5.2.4. pathman_partition_list
В этом представлении показываются все существующие разделы, а также их родители и границы диапазонов (NULL для хеш-секций).
-- вспомогательная функция, возвращающая множество CREATE OR REPLACE FUNCTION show_partition_list() RETURNS TABLE ( parent REGCLASS, partition REGCLASS, parttype INT4, expr TEXT, range_min TEXT, range_max TEXT) AS 'pg_pathman', 'show_partition_list_internal' LANGUAGE C STRICT; CREATE OR REPLACE VIEW pathman_partition_list AS SELECT * FROM show_partition_list();
F.30.5.3. Функции
F.30.5.3.1. Функции для создания секций
create_hash_partitions(parent_relid REGCLASS, expression TEXT, partitions_count INTEGER, partition_data BOOLEAN DEFAULT TRUE, partition_names TEXT[] DEFAULT NULL, tablespaces TEXT[] DEFAULT NULL)
Выполняет секционирование по хешу для таблицы relation
по целочисленному ключу expression
. Параметр partitions_count
задаёт число создаваемых секций; он не может быть изменён впоследствии. Если параметр partition_data
равен true
, все данные будут автоматически переноситься из родительской таблицы в секции. Заметьте, что перенос данных может занять некоторое время, и таблица будет заблокирована до завершения транзакции. Если вам нужно перенести данные без блокировки, воспользуйтесь функцией partition_table_concurrently()
. Для каждой секции вызывается обработчик создания секции, если он был установлен заранее (см. set_init_callback()
).
create_range_partitions(relation REGCLASS, expression TEXT, start_value ANYELEMENT, p_interval ANYELEMENT, p_count INTEGER DEFAULT NULL, partition_data BOOLEAN DEFAULT TRUE) create_range_partitions(relation REGCLASS, expression TEXT, start_value ANYELEMENT, p_interval INTERVAL, p_count INTEGER DEFAULT NULL, partition_data BOOLEAN DEFAULT TRUE) create_range_partitions(relation REGCLASS, expression TEXT, bounds ANYARRAY, partition_names TEXT[] DEFAULT NULL, tablespaces TEXT[] DEFAULT NULL, partition_data BOOLEAN DEFAULT TRUE)
Выполняет секционирование по диапазонам таблицы relation
по ключу разбиения expression
. Аргумент start_value
задаёт начальное значение, p_interval
задаёт диапазон по умолчанию для автоматически создаваемых секций или секций, создаваемых функциями append_range_partition()
или prepend_range_partition()
. Если в p_interval
передаётся NULL
, автоматическое создание секций отключается. В p_count
задаётся число заранее создаваемых секций (если p_count
не задано, pg_pathman
пытается определить число секций по значению expression
). Массив bounds
определяет границы секций, которые должны быть созданы. Этот массив можно построить, воспользовавшись функцией generate_range_bounds()
. Для каждой секции вызывается обработчик создания секции, если он был установлен заранее.
F.30.5.3.2. Функции для переноса данных
partition_table_concurrently(relation REGCLASS, batch_size INTEGER DEFAULT 1000, sleep_time FLOAT8 DEFAULT 1.0)
Запускает фоновый рабочий процесс для переноса данных из родительской таблицы в секции. Этот рабочий процесс копирует данные в коротких транзакциях небольшими блоками (до 10000 строк в транзакции) и поэтому не оказывает значительного влияния на работу пользователей.
stop_concurrent_part_task(relation REGCLASS)
Останавливает фоновый рабочий процесс, выполняющий задачу параллельного секционирования. Замечание: рабочий процесс завершается после того, как заканчивает перемещение текущей порции данных.
F.30.5.3.3. Триггеры
Для операций INSERT
и межсекционных UPDATE
триггеры более не требуются. При этом пользовательские триггеры поддерживаются:
Каждое добавление строки приводит к выполнению триггерных функций BEFORE/AFTER INSERT для соответствующей секции.
Каждое изменение строки приводит к выполнению триггерных функций BEFORE/AFTER UPDATE для соответствующей секции.
Каждое перемещение строки (межсекционное изменение) приводит к выполнению триггерных функций BEFORE UPDATE + BEFORE/AFTER DELETE + BEFORE/AFTER INSERT для соответствующих секций.
F.30.5.3.4. Функции для управления секциями
replace_hash_partition(old_partition REGCLASS, new_partition REGCLASS, lock_parent BOOLEAN DEFAULT TRUE)
Заменяет заданную секцию таблицы, секционированной по хешу, другой таблицей. Если lock_parent
имеет значение true
, операции INSERT
/UPDATE
/ALTER TABLE
в родительской таблице не допускаются.
split_range_partition(partition_relid REGCLASS, split_value ANYELEMENT, partition_name TEXT DEFAULT NULL, tablespace TEXT DEFAULT NULL)
Разбивает диапазонную секцию partition
на две по значению value
. Для создаваемой секции вызывается обработчик создания секции, если он задан.
merge_range_partitions(variadic partitions REGCLASS[])
Объединяет несколько соседних секций при разбиении по диапазонам. Секции автоматически упорядочиваются по расположению диапазонов. Все данные будут собраны в первой секции, а остальные секции будут удалены. Если остающаяся секция содержит дочерние секции, для добавляемых при объединении данных могут создаваться новые дочерние секции согласно существующему выражению разбиения.
append_range_partition(parent_relid REGCLASS, partition_name TEXT DEFAULT NULL, tablespace TEXT DEFAULT NULL)
Добавляет новую диапазонную секцию с интервалом pathman_config.range_interval
в конец списка секций.
prepend_range_partition(parent_relid REGCLASS, partition_name TEXT DEFAULT NULL, tablespace TEXT DEFAULT NULL)
Добавляет новую диапазонную секцию с интервалом pathman_config.range_interval
в начало списка секций.
add_range_partition(parent_relid REGCLASS, start_value ANYELEMENT, end_value ANYELEMENT, partition_name TEXT DEFAULT NULL, tablespace TEXT DEFAULT NULL)
Добавляет новую диапазонную секцию для таблицы relation
с заданными границами диапазона. Если в качестве start_value
или end_value
передаётся NULL, соответствующая граница диапазона будет бесконечной.
drop_range_partition(partition_relid TEXT, delete_data BOOLEAN DEFAULT TRUE)
Удаляет диапазонную секцию, а также все содержащиеся в ней данные, если установлен флаг delete_data
.
attach_range_partition(parent_relid REGCLASS, partition_relid REGCLASS, start_value ANYELEMENT, end_value ANYELEMENT)
Присоединяет секцию к существующему отношению с секционированием по диапазонам. Структура присоединяемой таблицы должна в точности повторять структуру родительской, включая удалённые столбцы. Если установлен, вызывается обработчик создания секции (см. Подраздел F.30.5.2.2).
detach_range_partition(partition_relid REGCLASS)
Отсоединяет секцию от существующего отношения с секционированием по диапазонам.
disable_pathman_for(parent_relid REGCLASS)
Полностью отключает механизм секционирования pg_pathman
для заданной родительской таблицы и удаляет триггер на добавление, если он существует. При этом все секции и данные остаются без изменений.
drop_partitions(parent_relid REGCLASS, delete_data BOOLEAN DEFAULT FALSE)
Удаляет секции родительской таблицы (как сторонние, так и локальные). Если параметр delete_data
равен false
(это значение по умолчанию), данные сначала копируются в родительскую таблицу.
F.30.5.3.5. Дополнительные функции
pathman_version()
Возвращает номер версии pg_pathman
.
set_interval(relation REGCLASS, value ANYELEMENT)
Изменяет интервал для таблицы, секционированной по диапазонам. Заметьте, что этот интервал должен быть неотрицательными и он не должен быть пустым, то есть его значение должно быть больше нуля для числовых типов, не меньше 1 микросекунды для типа timestamp
и не меньше 1 дня для типа date
.
set_enable_parent(relation REGCLASS, value BOOLEAN)
Включает/исключает родительскую таблицу в план запроса. В оригинальном планировщике Postgres Pro родительская таблица всегда включается в план запроса, даже если она пуста, что может повлечь дополнительные издержки. Вы можете исключить родительскую таблицу из рассмотрения, если не собираетесь никогда хранить в ней какие-либо данные. Значение по умолчанию зависит от параметра partition_data
, заданного при изначальном создании секций функцией create_range_partitions()
. Если параметр partition_data
имел значение true
, значит все данные уже были перенесены в секции, и родительская таблица отключается. В противном случае она включена.
set_auto(relation REGCLASS, value BOOLEAN)
Включает/отключает автоматическое создание секций (только для секционирования по диапазонам). По умолчанию этот режим включён.
set_init_callback(relation REGCLASS, callback REGPROCEDURE DEFAULT 0)
Устанавливает обработчик создания секции, который будет вызываться для каждой присоединяемой или создаваемой секции (по диапазонам или по хешу). Если обработчик имеет характеристику SECURITY INVOKER
, он выполняется от имени пользователя, который выполнял оператор, требующий создания новой секции. Например:
INSERT INTO partitioned_table VALUES (-5)
Обработчик должен иметь следующую сигнатуру: part_init_callback(args JSONB) RETURNS VOID
. В параметре arg
передаются несколько полей, зависящих от типа секционирования:
/* Таблица abc с секционированием по диапазонам (потомок abc_4) */ { "parent": "abc", "parttype": "2", "partition": "abc_4", "range_max": "401", "range_min": "301" } /* Таблица abc с секционированием по хешу (потомок abc_0) */ { "parent": "abc", "parttype": "1", "partition": "abc_0" }
set_spawn_using_bgw(relation REGCLASS, value BOOLEAN)
Включает использование SpawnPartitionsWorker для создания новых секций в отдельной транзакции в случае добавления данных за пределами диапазона разбиения.
create_naming_sequence(parent_relid REGCLASS)
Включает автоматическое именование секций для заданной таблицы relation
. Эту функцию необходимо использовать при разбиении таблицы по составному ключу.
add_to_pathman_config(parent_relid REGCLASS, expression TEXT, range_interval TEXT) add_to_pathman_config(parent_relid REGCLASS, expression TEXT)
Регистрирует указанную таблицу relation
в pg_pathman
для выполнения секционирования по заданному выражению (expression
). Для разбиения по диапазонам аргумент range_interval
является обязательным. Вы можете передать в нём NULL
, если планируете добавлять секции вручную.
generate_range_bounds(p_start ANYELEMENT, p_interval INTERVAL, p_count INTEGER) generate_range_bounds(p_start ANYELEMENT, p_interval ANYELEMENT, p_count INTEGER)
Строит массив bounds
с границами секций, которые должны быть созданы. Этот массив можно передать в качестве аргумента функции create_range_partitions()
.
F.30.6. Авторы
Ильдар Мусин
<i.musin@postgrespro.ru>
, Postgres Professional, РоссияАлександр Коротков
<a.korotkov@postgrespro.ru>
, Postgres Professional, РоссияДмитрий Иванов
<d.ivanov@postgrespro.ru>
, Postgres Professional, Россия