Не найдена секция таблицы табл 5 нпфл

У многих достижений версии PostgreSQL 10 прописка в разделе Секционирование (Partitioning). И это справедливо: очевидно, что при переходе от 9.6 к 10 произошел мощный технологический скачок. В предыдущих версиях секции строили и управляли ими, теми средствами, что уже имелись: механизмом наследования со всеми его ограничениями и неудобствами.

В версии 10 перешли к более специализированным механизмам и более привычному (в том числе для пользователей Oracle, а с этим приходится считаться) синтаксису. Этот скачок при переходе от 10 к версии 11 должен был подкрепиться важными дополнениями, которые должны расширить функциональность и улучшить производительность операций, использующих секционирование. Но из итогов последнего комитфеста (он закончился 8 апреля) видно, что не все задуманное удалось довести до рабочего состояния, а значит не все попадет в версию 11.

К тому же последние пару лет параллельно велись разработки модуля pg_pathman в Postgres Professional. Некоторые важные возможности пересеклись, некоторые остались уникальны для PostgreSQL и pg_pathman (который работает с ванильной версией, то есть PostgreSQL 10 + pg_pathman дает уже вполне впечатляющую сумму функциональности). Об этом будет отдельная статья. Замечания, относящиеся к версии 11 и к pg_pathman для удобства выделены курсивом.

Эта статья представляет собой переработанные и дополненные фрагменты книжки Nouveaulités de PostgreSQL 10. (с) Dalibo, перевод с французского Игоря Лёвшина (оригинал). Примеры из книги проверены, иногда адаптированы и локализованы для большей наглядности.

Старый подход к секционированию

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

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

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

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

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

Новый подход к секционированию

В версии 10 появилась новая система секционирования, основанная на инфраструктуре, уже существовавшей в PostgreSQL.

Целью разработчиков было упрощение создания и администрирования секционированных таблиц. К уже существующим SQL-операторам (например CREATE TABLE и ALTER TABLE) добавлены специальные предложения для создания, присоединения/отсоединения секций. Вот список изменений.

  • Создание и администрирование секций упростилось благодаря интеграции в ядро СУБД
  • Триггеры больше не нужны. Теперь:
    • вставка убыстрилась
    • появилось автоматическое перенаправление вставляемых данных в правильную секцию
    • генерируется ошибка в случае направления в неподходящую секцию
  • Теперь при работе с секциями:
    • можно присоединять/отсоединять секции
    • есть явные ограничения целостности секций
    • возможно секционирование по выражению в ключе разбиения
    • можно создавать подсекции
  • Изменения в системном каталоге:
    • новые столбцы в pg_class
    • новый каталог pg_partitioned_table

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

Каталог pg_class был модифицирован и теперь содержит такую информацию :

Каталог pg_partitioned_table содержит следующие столбцы :

Столбец Содержимое
partrelid OID записи в pg_class для этой секционированной таблицы
partstrat Стратегия секционирования: l = по списку (BY LIST), r = по диапазону (BY RANGE)
partnatts Число столбцов в ключе разбиения
partattrs Массив длины partnatts, указывающий, какие столбцы таблицы входят в ключ разбиения.*)
partclass Для каждого столбца в ключе разбиения этот массив содержит OID применяемых классов операторов.
partcollation Для каждого столбца в ключе разбиения этот массив содержит OID правила сортировки**)
partexprs Деревья выражений для частей ключа разбиения.***)

*) Например, значения 1 и 3 будут означать, что ключ разбиения составляют первый и третий столбцы таблицы. Ноль в этом массиве означает, что соответствующей частью ключа разбиения является выражение, а не ссылка на отдельный столбец.
**) Для секционирования либо 0, если тип данных этого столбца не сортируемый.
***) Деревья выражений (в представлении nodeToString()) для частей ключа разбиения, что не являющихся простыми ссылками на столбцы. Этот список содержит один элемент для каждого нулевого значения в partattrs. Значением может быть NULL, если все части ключа разбиения являются простыми указаниями столбцов.

Виды секционирования

  • по списку
  • по диапазону
  • по хешу
  • по выражению
  • по составному ключу
  • с подсекциями

PostgreSQL 10 поддерживает все, кроме третьего. Мы сейчас остановимся на двух первых.

Секционирование по списку значений

Создадим основную таблицу и ее секции :

habr_10=# CREATE TABLE parti_1(c1 integer, c2 text) PARTITION BY LIST (c1); CREATE TABLE habr_10=# CREATE TABLE parti_1_a PARTITION OF parti_1 FOR VALUES IN (1, 2, 3); CREATE TABLE habr_10=# CREATE TABLE parti_1_b PARTITION OF parti_1 FOR VALUES IN (4, 5); CREATE TABLE

Мы можем отсоединять и подсоединять секции :

Отсоединим :

habr_10=# ALTER TABLE parti_1 DETACH PARTITION parti_1_a;

Теперь это независимая таблица. Присоединим ее обратно :

habr_10=# ALTER TABLE parti_1 ATTACH PARTITION parti_1_a FOR VALUES IN (1, 2, 3);

Заполним ее данными. Сначала попробуем ввести значение не из списка. Если нет ни одной секции, соответствующей ключу разбиения, генерируется ошибка :

habr_10=# INSERT INTO parti_1 VALUES (0); ERROR: no PARTITION OF relation «parti_1» found for row DETAIL: Partition key of the failing row contains (c1) = (0). habr_10=# INSERT INTO parti_1 VALUES (6); ERROR: no PARTITION OF relation «parti_1» found for row DETAIL: Partition key of the failing row contains (c1) = (6).

Теперь введем корректные данные :

habr_10=# INSERT INTO parti_1 VALUES (1); INSERT 0 1 habr_10=# INSERT INTO parti_1 VALUES (2); INSERT 0 1 habr_10=# INSERT INTO parti_1 VALUES (5); INSERT 0 1 habr_10=# SELECT * FROM parti_1_a; c1 | c2 —-+—- 1 | 2 | (2 rows) habr_10=# SELECT * FROM parti_1_b; c1 | c2 —-+—- 5 | (1 row)

В процессе вставки данные корректно распределяются по секциям. Родительская секционированная таблица пуста: никакие данные не хранятся в родительской секционированной таблице. В этом можно убедиться, используя оператор SELECT с выражением ONLY :

habr_10=# SELECT * FROM ONLY parti_1; c1 | c2 —-+—- (0 rows)

Секционирование по диапазону

Создадим основную таблицу и одну секцию :

habr_10=# CREATE TABLE parti_2(c1 integer, c2 text) PARTITION BY RANGE (c1); CREATE TABLE habr_10=# CREATE TABLE parti_2_1 PARTITION OF parti_2 FOR VALUES FROM (1) to (100); CREATE TABLE

Заполним секцию данными :

habr_10=# INSERT INTO parti_2 VALUES (0); ERROR: no PARTITION OF relation «parti_2» found for row DETAIL: Partition key of the failing row contains (c1) = (0). habr_10=# INSERT INTO parti_2 VALUES (1); INSERT 0 1 habr_10=# INSERT INTO parti_2 VALUES (2); INSERT 0 1 habr_10=# INSERT INTO parti_2 VALUES (5); INSERT 0 1 habr_10=# INSERT INTO parti_2 VALUES (101); ERROR: no PARTITION OF relation «parti_2» found for row DETAIL: Partition key of the failing row contains (c1) = (101).

Если нет ни одной секции, соответствующей ключу разбиения, генерируется ошибка.

Составной ключ разбиения

Составной ключ разбиения работает только при разбиении по диапазонам. Создадим основную таблицу с составным ключом :

habr_10=# CREATE TABLE parti_3(c1 integer, c2 text, c3 date) PARTITION BY RANGE (c1, c3);

Добавим секцию, используя составной ключ разбиения :

habr_10=# CREATE TABLE parti_3_a PARTITION of parti_3 FOR VALUES FROM (1,’2017-08-10′) TO (100, ‘2017-08-11’);

Проиллюстрируем совместное использование разных табличных пространств на примере ниже. Начнем с создания табличных пространств. Напомним, что каталог табличного пространства должен существовать, быть пустым и принадлежать пользователю ОС, под которым запущен Postgres. Например:

test#mkdir /tmp/tablespaces/ts0 test#sudo chown postgres /tmp/tablespaces/ts0

Теперь :

habr_10=# CREATE TABLESPACE ts0 LOCATION ‘/tmp/tablespaces/ts0’; CREATE TABLESPACE habr_10=# CREATE TABLESPACE ts1 LOCATION ‘/tmp/tablespaces/ts1’; CREATE TABLESPACE habr_10=# CREATE TABLESPACE ts2 LOCATION ‘/tmp/tablespaces/ts2’; CREATE TABLESPACE habr_10=# CREATE TABLESPACE ts3 LOCATION ‘/tmp/tablespaces/ts3’; CREATE TABLESPACE

Создадим секционированную таблицу и две секции :

habr_10=# DROP TABLE parti_3; DROP TABLE

(Секции удаляются каскадно)

habr_10=# CREATE TABLE parti_3(c1 integer, c2 text, c3 date not null) PARTITION BY RANGE (c1, c3); CREATE TABLE habr_10=# CREATE TABLE parti_3_1 PARTITION OF parti_3 FOR VALUES FROM (1,’2017-08-10′) TO (100, ‘2017-08-11′) TABLESPACE ts1; CREATE TABLE habr_10=# CREATE TABLE parti_3_2 PARTITION OF parti_3 FOR VALUES FROM (100,’2017-08-11’) TO (200, ‘2017-08-12’) TABLESPACE ts2; CREATE TABLE

Если величины не выходят за границы секций:

habr_10=# INSERT INTO parti_3 VALUES (1, ‘test’, ‘2017-08-10’); INSERT 0 1 habr_10=# INSERT INTO parti_3 VALUES (150, ‘test2’, ‘2017-08-11’); INSERT 0 1

Если величина c1 слишком мала :

habr_10=# INSERT INTO parti_3 VALUES (0, ‘test’, ‘2017-08-10’); ERROR: no partition of relation «parti_3» found for row DETAIL : Partition key of the failing row contains (c1, c3) = (0, 2017-08-10).

Если величина c3 (столбец с типом данных date) предшествует нижней границе временного диапазона :

habr_10=# INSERT INTO parti_3 VALUES (1, ‘test’, ‘2017-08-09’); ERROR: no partition of relation «parti_3» found for row DETAIL : Partition key of the failing row contains (c1, c3) = (1, 2017-08-09).

Специальные величины MINVALUE и MAXVALUE позволяют не указывать величину одной из границ. Например, секции parti_3_0 и parti_3_3 можно объявить как показано ниже и успешно вставить строки, которые выше вызвали сообщение об ошибке.

Внимание: некоторые онлайн-статьи, которые были опубликованы до выпуска версии beta3, упоминают специальное значение UNBOUNDED, которое потом было заменено MINVALUE и MAXVALUE.

habr_10=# CREATE TABLE parti_3_0 PARTITION OF parti_3 FOR VALUES FROM (MINVALUE, MINVALUE) TO (1,’2017-08-10′) TABLESPACE ts0; habr_10=# CREATE TABLE parti_3_3 PARTITION OF parti_3 FOR VALUES FROM (200,’2017-08-12′) TO (MAXVALUE, MAXVALUE) TABLESPACE ts3;

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

habr_10=# ANALYZE parti_3; ANALYZE habr_10=# SELECT relname,relispartition,relkind,reltuples FROM pg_class WHERE relname LIKE ‘parti_3%’; relname | relispartition | relkind | reltuples ————-+—————-+———+———— parti_3 | f | p | 0 parti_3_1 | t | r | 1 parti_3_2 | t | r | 1 (5 rows)

Секции по умолчанию и автоматическое создание секций

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

В модуле pg_pathman есть очень удобные функции, позволяющие не думать о том, попадет ли значение в отведенные границы диапазона. Там можно задавать интервал значений для секций, и их сам модуль создаст их столько, сколько нужно, чтобы вместить введенные значения. Это будет работать на секциях вида BY RANGE.

Надо иметь в виду, что в PostgreSQL 10 нет секционирования по хешу. Этот серьезный недостаток поправили в 11: соответствующий патч прошел комитфест, так что остается ждать выхода PostgreSQL 11. Если секционирование по хешу необходимо или желательно прямо сейчас, можно воспользоваться pg_pathman.

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

Таблица no_parti — несекционированная таблица. Cоздаем ее следующим образом :

CREATE TABLE no_parti (c1 integer, c2 text); INSERT INTO no_parti SELECT i, ‘something’ FROM generate_series(0, 9999999) i; Time: 10097.098 ms (00:10.097)

Таблицу parti_new создаем с помощью новой функциональности версии PostgreSQL 10 :

CREATE TABLE parti_new (c1 integer, c2 text) PARTITION BY RANGE (c1); CREATE TABLE parti_new_1 PARTITION OF parti_new FOR VALUES FROM ( 0) TO ( 1000000); CREATE TABLE parti_new_2 PARTITION OF parti_new FOR VALUES FROM (1000000) TO ( 2000000); CREATE TABLE parti_new_3 PARTITION OF parti_new FOR VALUES FROM (2000000) TO ( 3000000); CREATE TABLE parti_new_4 PARTITION OF parti_new FOR VALUES FROM (3000000) TO ( 4000000); CREATE TABLE parti_new_5 PARTITION OF parti_new FOR VALUES FROM (4000000) TO ( 5000000); CREATE TABLE parti_new_6 PARTITION OF parti_new FOR VALUES FROM (5000000) TO ( 6000000); CREATE TABLE parti_new_7 PARTITION OF parti_new FOR VALUES FROM (6000000) TO ( 7000000); CREATE TABLE parti_new_8 PARTITION OF parti_new FOR VALUES FROM (7000000) TO ( 8000000); CREATE TABLE parti_new_9 PARTITION OF parti_new FOR VALUES FROM (8000000) TO ( 9000000); CREATE TABLE parti_new_0 PARTITION OF parti_new FOR VALUES FROM (9000000) TO (10000000); INSERT INTO parti_new SELECT i, ‘something’ FROM generate_series(0, 9999999) i; Time: 11448.867 ms (00:11.449)

Таблицу parti_old создаем с помощью старого метода секционирования :

CREATE TABLE parti_old (c1 integer, c2 text); CREATE TABLE parti_old_1 (CHECK (c1 BETWEEN 0 AND 1000000)) INHERITS (parti_old); CREATE TABLE parti_old_2 (CHECK (c1 BETWEEN 1000000 AND 2000000)) INHERITS (parti_old); CREATE TABLE parti_old_3 (CHECK (c1 BETWEEN 2000000 AND 3000000)) INHERITS (parti_old); CREATE TABLE parti_old_4 (CHECK (c1 BETWEEN 3000000 AND 4000000)) INHERITS (parti_old); CREATE TABLE parti_old_5 (CHECK (c1 BETWEEN 4000000 AND 5000000)) INHERITS (parti_old); CREATE TABLE parti_old_6 (CHECK (c1 BETWEEN 5000000 AND 6000000)) INHERITS (parti_old); CREATE TABLE parti_old_7 (CHECK (c1 BETWEEN 6000000 AND 7000000)) INHERITS (parti_old); CREATE TABLE parti_old_8 (CHECK (c1 BETWEEN 7000000 AND 8000000)) INHERITS (parti_old); CREATE TABLE parti_old_9 (CHECK (c1 BETWEEN 8000000 AND 9000000)) INHERITS (parti_old); CREATE TABLE parti_old_0 (CHECK (c1 BETWEEN 9000000 AND 10000000)) INHERITS (parti_old); CREATE OR REPLACE FUNCTION insert_into() RETURNS TRIGGER LANGUAGE plpgsql AS $FUNC$ BEGIN IF NEW.c1 BETWEEN 0 AND 1000000 THEN INSERT INTO parti_old_1 VALUES (NEW.*); ELSIF NEW.c1 BETWEEN 1000000 AND 2000000 THEN INSERT INTO parti_old_2 VALUES (NEW.*); ELSIF NEW.c1 BETWEEN 2000000 AND 3000000 THEN INSERT INTO parti_old_3 VALUES (NEW.*); ELSIF NEW.c1 BETWEEN 3000000 AND 4000000 THEN INSERT INTO parti_old_4 VALUES (NEW.*); ELSIF NEW.c1 BETWEEN 4000000 AND 5000000 THEN INSERT INTO parti_old_5 VALUES (NEW.*); ELSIF NEW.c1 BETWEEN 5000000 AND 6000000 THEN INSERT INTO parti_old_6 VALUES (NEW.*); ELSIF NEW.c1 BETWEEN 6000000 AND 7000000 THEN INSERT INTO parti_old_7 VALUES (NEW.*); ELSIF NEW.c1 BETWEEN 7000000 AND 8000000 THEN INSERT INTO parti_old_8 VALUES (NEW.*); ELSIF NEW.c1 BETWEEN 8000000 AND 9000000 THEN INSERT INTO parti_old_9 VALUES (NEW.*); ELSIF NEW.c1 BETWEEN 9000000 AND 10000000 THEN INSERT INTO parti_old_0 VALUES (NEW.*); END IF; RETURN NULL; END; $FUNC$; CREATE TRIGGER tr_insert_parti_old BEFORE INSERT ON parti_old FOR EACH ROW EXECUTE PROCEDURE insert_into(); INSERT INTO parti_old SELECT i, ‘something’ FROM generate_series(0, 9999999) i; Time: 125351.918 ms (02:05.352)

Мы видим, что скорость вставки при новом подходе к секционированию на порядок выше.

В версию 11 попал патч, существенно увеличивающий скорость вставки в случае, когда данные в ключе разбиения растут монотонно или почти монотонно.

Что касается производительности чтения, то на этапе 10 -> 11 произошли принципиальные изменения: появилась возможность эффективно исключать из плана секции, в которых заведомо нет данных (partition pruning). Это можно будет делать и на этапе исполнения, когда заранее не известно условие попадания в ту или иную секцию. Так случается, например, в случаях подзапроса.

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

Сравнение старого и нового секционирования на примерах

Разберемся в различиях подхода к секционированию в версиях 9.6 и 10.

Создадим таблицу температур, привязанных к месту и дате. Будем создавать секции для каждого места и каждого месяца.

Команды для создания таких таблиц в 9.6 :

CREATE TABLE meteo ( t_id serial, place text NOT NULL, hour_mesure timestamp DEFAULT now(), temperature real NOT NULL ); CREATE TABLE meteo_moscow_201709 ( CHECK ( place = ‘Moscow’ AND hour_mesure >= TIMESTAMP ‘2017-09-01 00:00:00’ AND hour_mesure < TIMESTAMP ‘2017-10-01 00:00:00’ ) ) INHERITS (meteo); CREATE TABLE meteo_moscow_201710 ( CHECK ( place = ‘Moscow’ AND hour_mesure >= TIMESTAMP ‘2017-10-01 00:00:00’ AND hour_mesure < TIMESTAMP ‘2017-11-01 00:00:00’ ) ) INHERITS (meteo); CREATE TABLE meteo_sochi_201709 ( CHECK ( place = ‘Sochi’ AND hour_mesure >= TIMESTAMP ‘2017-09-01 00:00:00’ AND hour_mesure < TIMESTAMP ‘2017-10-01 00:00:00’ ) ) INHERITS (meteo); CREATE TABLE meteo_sochi_201710 ( CHECK ( place = ‘Sochi’ AND hour_mesure >= TIMESTAMP ‘2017-10-01 00:00:00’ AND hour_mesure < TIMESTAMP ‘2017-11-01 00:00:00’ ) ) INHERITS (meteo); CREATE TABLE meteo_magadan_201709 ( CHECK ( place = ‘Magadan’ AND hour_mesure >= TIMESTAMP ‘2017-09-01 00:00:00’ AND hour_mesure < TIMESTAMP ‘2017-10-01 00:00:00’ ) ) INHERITS (meteo); CREATE TABLE meteo_magadan_201710 ( CHECK ( place = ‘Magadan’ AND hour_mesure >= TIMESTAMP ‘2017-10-01 00:00:00’ AND hour_mesure < TIMESTAMP ‘2017-11-01 00:00:00’ ) ) INHERITS (meteo); CREATE OR REPLACE FUNCTION meteo_insert_trigger() RETURNS TRIGGER AS $$ BEGIN IF ( NEW.place = ‘Moscow’ ) THEN IF ( NEW.hour_mesure >= TIMESTAMP ‘2017-09-01 00:00:00’ AND NEW.hour_mesure < TIMESTAMP ‘2017-10-01 00:00:00’ ) THEN INSERT INTO meteo_moscow_201709 VALUES (NEW.*); ELSIF ( NEW.hour_mesure >= TIMESTAMP ‘2017-10-01 00:00:00’ AND NEW.hour_mesure < TIMESTAMP ‘2017-11-01 00:00:00’ ) THEN INSERT INTO meteo_moscow_201710 VALUES (NEW.*); ELSE RAISE EXCEPTION ‘Date does not fit meteo_insert_trigger(Moscow)’; END IF; ELSIF ( NEW.place = ‘Sochi’ ) THEN IF ( NEW.hour_mesure >= TIMESTAMP ‘2017-09-01 00:00:00’ AND NEW.hour_mesure < TIMESTAMP ‘2017-10-01 00:00:00’ ) THEN INSERT INTO meteo_sochi_201709 VALUES (NEW.*); ELSIF ( NEW.hour_mesure >= TIMESTAMP ‘2017-10-01 00:00:00’ AND NEW.hour_mesure < TIMESTAMP ‘2017-11-01 00:00:00’ ) THEN INSERT INTO meteo_sochi_201710 VALUES (NEW.*); ELSE RAISE EXCEPTION ‘Date does not fit meteo_insert_trigger(Sochi)’; END IF; ELSIF ( NEW.place = ‘Magadan’ ) THEN IF ( NEW.hour_mesure >= TIMESTAMP ‘2017-09-01 00:00:00’ AND NEW.hour_mesure < TIMESTAMP ‘2017-10-01 00:00:00’ ) THEN INSERT INTO meteo_magadan_201709 VALUES (NEW.*); ELSIF ( NEW.hour_mesure >= TIMESTAMP ‘2017-10-01 00:00:00’ AND NEW.hour_mesure < TIMESTAMP ‘2017-11-01 00:00:00’ ) THEN INSERT INTO meteo_magadan_201710 VALUES (NEW.*); ELSE RAISE EXCEPTION ‘Date does not fit meteo_insert_trigger(Magadan)’; END IF; ELSE RAISE EXCEPTION ‘Date does not fit meteo_insert_trigger() !’; END IF; RETURN NULL; END; $$ LANGUAGE plpgsql; CREATE TRIGGER insert_meteo_trigger BEFORE INSERT ON meteo FOR EACH ROW EXECUTE PROCEDURE meteo_insert_trigger();

Команды для создания таких таблиц в 10 ;

CREATE TABLE meteo ( t_id integer GENERATED BY DEFAULT AS IDENTITY, place text NOT NULL, hour_mesure timestamp DEFAULT now(), temperature real NOT NULL ) PARTITION BY RANGE (place, hour_mesure); CREATE TABLE meteo_moscow_201709 PARTITION of meteo FOR VALUES FROM (‘Moscow’, ‘2017-09-01 00:00:00’) TO (‘Moscow’, ‘2017-10-01 00:00:00’); CREATE TABLE meteo_moscow_201710 PARTITION of meteo FOR VALUES FROM (‘Moscow’, ‘2017-10-01 00:00:00’) TO (‘Moscow’, ‘2017-11-01 00:00:00’); CREATE TABLE meteo_sochi_201709 PARTITION of meteo FOR VALUES FROM (‘Sochi’, ‘2017-09-01 00:00:00’) TO (‘Sochi’, ‘2017-10-01 00:00:00’); CREATE TABLE meteo_sochi_201710 PARTITION of meteo FOR VALUES FROM (‘Sochi’, ‘2017-10-01 00:00:00’) TO (‘Sochi’, ‘2017-11-01 00:00:00’); CREATE TABLE meteo_paris_201709 PARTITION of meteo FOR VALUES FROM (‘Magadan’, ‘2017-09-01 00:00:00’) TO (‘Magadan’, ‘2017-10-01 00:00:00’); CREATE TABLE meteo_paris_201710 PARTITION of meteo FOR VALUES FROM (‘Magadan’, ‘2017-10-01 00:00:00’) TO (‘Magadan’, ‘2017-11-01 00:00:00’);

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

(В pg_pathman разработчики пошли еще дальше. Там доступны функции, которые разрезают существующие секции, объединяют соседние и многие другие.)

Вот пример функции, создающей случайные строки в таблице :

CREATE OR REPLACE FUNCTION populate_meteo() RETURNS TEXT AS $$ DECLARE placex text := ‘{}’; v_place text; v_hour timestamp; v_temperature real; v_nb_insertions integer := 500000; v_insertion integer; BEGIN placex=’Moscow’; placex=’Sochi’; placex=’Magadan’; FOR v_insertion IN 1 .. v_nb_insertions LOOP v_place=placex; v_hour=’2017-09-01′::timestamp + make_interval(days => floor((random()*60))::int, secs => floor((random()*86400))::int); v_temperature:=round(((random()*14))::numeric+10,2); IF EXTRACT(MONTH FROM v_hour) = 10 THEN v_temperature:=v_temperature-4; END IF; IF EXTRACT(HOUR FROM v_hour) <= 9 OR EXTRACT(HOUR FROM v_hour) >= 20 THEN v_temperature:=v_temperature-5; ELSEIF EXTRACT(HOUR FROM v_hour) >= 12 AND EXTRACT(HOUR FROM v_hour) <= 17 THEN v_temperature:=v_temperature+5; END IF; INSERT INTO meteo (place,hour_mesure,temperature) VALUES (v_place,v_hour,v_temperature); END LOOP; RETURN v_nb_insertions||’ mesures de température insérées’; END; $$ LANGUAGE plpgsql;

Вставим строки в таблицы meteo из habr_9_6 и habr_10 :

habr_9_6=# EXPLAIN ANALYSE SELECT populate_meteo(); QUERY PLAN —————————————————————————————— Result (cost=0.00..0.26 rows=1 width=32) (actual time=33315.067..33315.068 rows=1 loops=1) Planning time: 0.034 ms Execution time: 33315.084 ms (3 rows) habr_10=# EXPLAIN ANALYSE SELECT populate_meteo(); QUERY PLAN ————————————————————————————— Result (cost=0.00..0.26 rows=1 width=32) (actual time=14976.438..14976.438 rows=1 loops= 1) Planning time: 0.016 ms Execution time: 14976.499 ms (3 rows)

Мы видим, что в версии 10 данные вставляются вдвое быстрее.

Управление всем семейством секций сразу

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

habr_9_6=# SELECT ‘VACUUM ANALYZE ‘||relname AS operation FROM pg_stat_user_tables WHERE relname LIKE ‘meteo_%’; operation ———————————— VACUUM ANALYZE meteo_moscow_201709 VACUUM ANALYZE meteo_moscow_201710 VACUUM ANALYZE meteo_sochi_201709 VACUUM ANALYZE meteo_sochi_201710 VACUUM ANALYZE meteo_paris_201709 VACUUM ANALYZE meteo_paris_201710 (6 rows) habr_9_6=# \gexec VACUUM VACUUM VACUUM VACUUM VACUUM VACUUM

В версии 10 для того, чтобы произвести VACUUM и ANALYSE всех секций, достаточно обратиться только к родительской таблице :

habr_10=# VACUUM ANALYZE meteo; VACUUM habr_10=# SELECT now() AS date,relname,last_vacuum,last_analyze FROM pg_stat_user_tables WHERE relname LIKE ‘meteo_sochi%’; -+—————————— date | 2018-04-06 23:38:48.59511+03 relname | meteo_sochi_201709 last_vacuum | 2018-04-06 23:37:05.931573+03 last_analyze | 2018-04-06 23:37:05.958845+03 -+—————————— date | 2018-04-06 23:38:48.59511+03 relname | meteo_sochi_201710 last_vacuum | 2018-04-06 23:37:05.973254+03 last_analyze | 2018-04-06 23:37:06.002487+03

Ограничения версии 10

В версии 10 создание индекса на секционированной таблице по-прежнему невозможно :

habr_10=# CREATE INDEX meteo_hour_mesure_idx ON meteo (hour_mesure); ERROR: cannot create index on partitioned table «meteo»

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

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

В то же время, как и в версии 9.6, можно создавать индексы на отдельных секциях :

habr_10=# CREATE INDEX meteo_moscow_201710_hour_idx ON meteo_moscow_201710 (hour_mesure); CREATE INDEX

В версии 11 ограничение уникальности будет работать (в случае когда ограничение наложено на столбец с ключом разбиения). Это большое достижение, и оно дает возможность ссылаться на секционированную таблицу из внешнего ключа другой таблицы (FOREIGN KEY)

Обновление данных

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

habr_10=# UPDATE meteo SET place=’Sochi’ WHERE place=’Moscow’; ERROR: new row for relation «meteo_moscow_201709» violates partition constraint DETAIL : Failing row contains (5, Sochi, 2017-09-15 05:09:23, 9.43).

В версии 11 можно смело обновлять значение ключа разбиения: строка автоматически переместится в нужную секцию.

Вставка данных за пределами границ секций

При декларативном секционировании версии 10 можно объявлять границы :

CREATE TABLE meteo_moscow_ancienne PARTITION of meteo FOR VALUES FROM (‘Moscow’, MINVALUE) TO (‘Moscow’, ‘2017-09-01 00:00:00’); CREATE TABLE meteo_sochi_ancienne PARTITION of meteo FOR VALUES FROM (‘Sochi’, MINVALUE) TO (‘Sochi’, ‘2017-09-01 00:00:00’); CREATE TABLE meteo_paris_ancienne PARTITION of meteo FOR VALUES FROM (‘Magadan’, MINVALUE) TO (‘Magadan’, ‘2017-09-01 00:00:00’);

Другие ограничения

  • Родительская таблица не может содержать данных
  • В секциях не может быть дополнительных относительно родительской таблицы столбцов
  • Множественное наследование не допускается
  • Значения NULL в секциях допускаются только в том случае, если они допускаются в секционированной (родительской) таблице
  • Секции, не принадлежащие данному экземпляру СУБД, не поддерживаются (но можно присоединять секцию как FDW — CREATE FOREIGN TABLE … PARTITION OF …).
  • В случае присоединения секции
    • необходима проверка ограничений целостности (приводящей к блокировке всей секции)
    • требуется идентичное родительской таблице ограничение CHECK.

Эти ограничения актуальны и для версии 11.

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