Секционирование существующих таблиц с журналом Робота и Оркестратора для PostgreSQL
Общая информация о механизме секционирования приведена на официальном сайте PostgreSQL .
1. Секционирование по месяцам
Для организации помесячного секционирования существующую таблицу необходимо подключить к секционированной таблице как раздел с историческими данными.
1.1. Журнал Робота (таблица Logs)
Создаётся новая головная секционированная таблица, структурно идентичная исходной таблице Logs, но с другим именем — например, Logs2:
CREATE TABLE public."Logs2"
(
"Id" uuid NOT NULL,
"OrchTimestampUtc" timestamp without time zone NOT NULL,
"Event" integer,
"EntityId" text COLLATE pg_catalog."default",
"UserId" text COLLATE pg_catalog."default",
"TimestampUtc" timestamp without time zone,
"OperationKey" uuid,
"Signature" text COLLATE pg_catalog."default",
"WorkerAdminName" text COLLATE pg_catalog."default",
"Text" text COLLATE pg_catalog."default",
"EventType" integer,
"IP" text COLLATE pg_catalog."default",
"TenantId" text COLLATE pg_catalog."default",
"AssignmentId" integer,
"RobotId" integer,
"RobotKey" text COLLATE pg_catalog."default",
"ProjectId" integer,
"WorkerId" integer,
"Type" integer,
"ParsedDateEvent" timestamp without time zone,
"ParsedType" integer,
"ParsedElementName" text COLLATE pg_catalog."default",
"ParsedElementId" text COLLATE pg_catalog."default",
"ParsedElementClass" text COLLATE pg_catalog."default",
"ParsedMessage" text COLLATE pg_catalog."default",
"ScreenFilePath" text COLLATE pg_catalog."default",
"OrchScreenFilePath" text COLLATE pg_catalog."default",
"OrchScreenFilePathThumb" text COLLATE pg_catalog."default",
"Node" text COLLATE pg_catalog."default",
"ExtDataJson" text COLLATE pg_catalog."default",
"RobotName" text COLLATE pg_catalog."default",
"ProjectName" text COLLATE pg_catalog."default",
"WorkerName" text COLLATE pg_catalog."default",
"RdpUserName" text COLLATE pg_catalog."default",
"LogType" integer NOT NULL,
"ParsedElementNum" text COLLATE pg_catalog."default"
) partition by range ("OrchTimestampUtc");Далее создаётся секция для будущих данных. В примере предполагается, что начиная с 16 февраля 2026 года новых записей ещё нет, и секции будут помесячными. При развёртывании необходимо подставить актуальные даты.
create table "Logs_20260216" partition of "Logs2"
for values from ('2026-02-16') to ('2026-03-01');Для подготовки существующей таблицы Logs к присоединению добавляется ограничение на поле OrchTimestampUtc и выполняется его валидация на всех данных:
begin;
set local statement_timeout to '1s';
alter table "Logs" add constraint "Logs_partbound_check"
check ("OrchTimestampUtc" < '2026-02-16' and "OrchTimestampUtc" is not null) not valid;
commit;
alter table "Logs" validate constraint "Logs_partbound_check";Если validate constraint завершается ошибкой, необходимо проанализировать содержимое таблицы, устранить несоответствия (например, удалить строки с недопустимыми датами) и повторить операцию. Важно: всю миграцию следует завершить до наступления граничной даты '2026-02-16'. В противном случае ограничение можно удалить и начать процедуру заново:
alter table "Logs" drop constraint "Logs_partbound_check";После успешной валидации выполняется переименование и подключение архивной таблицы как секции:
begin;
set statement_timeout to '1s';
alter table "Logs" rename to "Logs_archive";
alter table "Logs2" rename to "Logs";
alter table "Logs" attach partition "Logs_archive" for values from (MINVALUE) to ('2026-02-16');
commit;До наступления '2026-02-16' все новые данные будут продолжать записываться в секцию Logs_archive (бывшую таблицу Logs). После этой даты запись пойдёт в Logs_20260216. Все запросы к таблице Logs остаются прозрачными для приложений.
Для корректной работы на вновь созданных секциях необходимо воссоздать индексы. Если секций несколько, индексы создаются на каждой:
CREATE INDEX "IX_Logs_20260216_EntityId"
ON public."Logs_20260216" USING btree
("EntityId" COLLATE pg_catalog."default" ASC NULLS LAST)
TABLESPACE pg_default;
CREATE INDEX "IX_Logs_20260216_Event"
ON public."Logs_20260216" USING btree
("Event" ASC NULLS LAST)
TABLESPACE pg_default;
CREATE INDEX IF NOT EXISTS "IX_Logs_20260216_Monitoring"
ON public."Logs_20260216" USING btree
("RobotId" DESC NULLS LAST, "OrchTimestampUtc" DESC NULLS LAST, "LogType" ASC NULLS LAST, "Type" DESC NULLS LAST, "OperationKey" ASC NULLS LAST)
INCLUDE("RdpUserName")
TABLESPACE pg_default;
CREATE INDEX IF NOT EXISTS "IX_Logs_20260216_Monitoring_1"
ON public."Logs_20260216" USING btree
("Type" DESC NULLS LAST, "LogType" ASC NULLS LAST, "OperationKey" ASC NULLS LAST, "OrchTimestampUtc" DESC NULLS LAST)
TABLESPACE pg_default;
CREATE INDEX IF NOT EXISTS "IX_Logs_20260216_Monitoring_2"
ON public."Logs_20260216" USING btree
("OrchTimestampUtc" DESC NULLS LAST, "RobotId" DESC NULLS LAST, "LogType" ASC NULLS LAST)
TABLESPACE pg_default;
CREATE INDEX IF NOT EXISTS "IX_Logs_20260216_Monitoring_3"
ON public."Logs_20260216" USING btree
("LogType" ASC NULLS LAST, "OrchTimestampUtc" DESC NULLS LAST, "ProjectId" DESC NULLS LAST, "OperationKey" ASC NULLS LAST)
TABLESPACE pg_default;
CREATE INDEX "IX_Logs_20260216_OperationKey"
ON public."Logs_20260216" USING btree
("OperationKey" ASC NULLS LAST)
TABLESPACE pg_default;
CREATE INDEX "IX_Logs_20260216_ProjectId"
ON public."Logs_20260216" USING btree
("ProjectId" ASC NULLS LAST)
TABLESPACE pg_default;
CREATE INDEX "IX_Logs_20260216_RobotId"
ON public."Logs_20260216" USING btree
("RobotId" ASC NULLS LAST)
TABLESPACE pg_default;
CREATE INDEX "IX_Logs_20260216_RobotId_OperationKey_TimestampUtc"
ON public."Logs_20260216" USING btree
("RobotId" ASC NULLS LAST, "OperationKey" ASC NULLS LAST, "TimestampUtc" DESC NULLS LAST)
INCLUDE("Type", "LogType")
TABLESPACE pg_default;
CREATE INDEX "IX_Logs_20260216_RobotKey"
ON public."Logs_20260216" USING btree
("RobotKey" COLLATE pg_catalog."default" ASC NULLS LAST)
TABLESPACE pg_default;
CREATE INDEX "IX_Logs_20260216_UserId"
ON public."Logs_20260216" USING btree
("UserId" COLLATE pg_catalog."default" ASC NULLS LAST)
TABLESPACE pg_default;
CREATE INDEX "IX_Logs_20260216_WorkerId"
ON public."Logs_20260216" USING btree
("WorkerId" ASC NULLS LAST)
TABLESPACE pg_default;1.2. Журнал Оркестратора (таблица OrchEvents)
Аналогичные операции выполняются для таблицы OrchEvents. Создаётся секционированная головная таблица:
CREATE TABLE public."OrchEvents2"
(
"Id" uuid NOT NULL,
"OrchTimestampUtc" timestamp without time zone NOT NULL,
"Event" integer NOT NULL,
"EntityId" text COLLATE pg_catalog."default",
"UserId" text COLLATE pg_catalog."default",
"OperationKey" uuid,
"Signature" text COLLATE pg_catalog."default",
"WorkerAdminName" text COLLATE pg_catalog."default",
"Text" text COLLATE pg_catalog."default",
"EventType" integer,
"IP" text COLLATE pg_catalog."default",
"TenantId" text COLLATE pg_catalog."default",
"AssignmentId" integer,
"LogType" integer NOT NULL DEFAULT 1,
"ProjectId" integer,
"ProjectName" text COLLATE pg_catalog."default",
"NodeId" integer
) partition by range ("OrchTimestampUtc");Создаётся будущая секция:
create table "OrchEvents_20260216" partition of "OrchEvents2"
for values from ('2026-02-16') to ('2026-03-01');Подготавливаем существующую таблицу с данными OrchEvents – добавляем в неё ограничение на поле OrchTimestampUtc и проверяем это ограничение на данных таблицы:
Добавляется и проверяется ограничение на архивной таблице:
begin;
set local statement_timeout to '1s';
alter table "OrchEvents" add constraint "OrchEvents_partbound_check"
check ("OrchTimestampUtc" < '2026-02-16' and "OrchTimestampUtc" is not null) not valid;
commit;
alter table "OrchEvents" validate constraint "OrchEvents_partbound_check";Переименование таблицы и подключение секции:
begin;
set statement_timeout to '1s';
alter table "OrchEvents" rename to "OrchEvents_archive";
alter table "OrchEvents2" rename to "OrchEvents";
alter table "OrchEvents" attach partition "OrchEvents_archive" for values from (MINVALUE) to ('2026-02-16');
commit;Создание индексов на секции будущих данных OrchEvents:
CREATE INDEX "IX_OrchEvents_20260216_AllOrchTimestampUtc"
ON public."OrchEvents_20260216" USING btree
("TenantId" COLLATE pg_catalog."default" ASC NULLS LAST, "Event" ASC NULLS LAST, "UserId" COLLATE pg_catalog."default" ASC NULLS LAST, "EventType" ASC NULLS LAST, "OrchTimestampUtc" DESC NULLS LAST)
INCLUDE("EntityId", "OperationKey", "Signature", "WorkerAdminName", "Text", "IP", "AssignmentId", "LogType")
TABLESPACE pg_default;
CREATE INDEX "IX_OrchEvents_20260216_EntityId_OrchTimestampUtc"
ON public."OrchEvents_20260216" USING btree
("EntityId" COLLATE pg_catalog."default" ASC NULLS LAST, "OrchTimestampUtc" ASC NULLS LAST)
TABLESPACE pg_default;
CREATE INDEX "IX_OrchEvents_20260216_Event_OrchTimestampUtc"
ON public."OrchEvents_20260216" USING btree
("Event" ASC NULLS LAST, "OrchTimestampUtc" ASC NULLS LAST)
TABLESPACE pg_default;
CREATE INDEX "IX_OrchEvents_20260216_UserId_OrchTimestampUtc"
ON public."OrchEvents_20260216" USING btree
("UserId" COLLATE pg_catalog."default" ASC NULLS LAST, "OrchTimestampUtc" ASC NULLS LAST)
TABLESPACE pg_default;При необходимости можно заранее создать несколько будущих секций. Последующее автоматическое создание и удаление секций реализуется с помощью планировщика.
2. Автоматическое создание новых секций
Для автоматического обслуживания используются расширение pg_cron и хранимая процедура.
2.1. Установка и настройка pg_cron
Установочный пакет копируется в доступную директорию и устанавливается (на примере дистрибутивов на базе RHEL 8, PostgreSQL 13):
# cd /srv/samba/shared/install/postgresql-13Устанавливаем пакет:
# dnf -y install 'pg_cron_13-1.3.0-1.rhel8.x86_64.rpm'В файл postgresql.conf вносятся изменения:
-
В параметр
shared_preload_librariesдобавляется (или дописывается)pg_cron:# vim /var/lib/pgsql/13/data/postgresql.conf shared_preload_libraries = 'pg_cron'
-
В секции настроек планировщика включается фильтрация секций (если ещё не включена):
enable_partition_pruning = on
-
Указывается целевая база данных для cron-заданий:
cron.database_name = 'ltoolslogs'
-
При необходимости задаётся часовой пояс, отличный от GMT, например:
cron.timezone = 'Asia/Omsk'
Для беспарольного подключения локальных задач редактируется pg_hba.conf:
# vim /var/lib/pgsql/13/data/pg_hba.conf
host all all 127.0.0.1/32 trust
host all all ::1/128 trust
После сохранения изменений служба PostgreSQL перезапускается:
# systemctl restart postgresql-13Расширение активируется в БД ltoolslogs. Можно через какой-то менеджер для работы с PostgreSQL, например, pgAdmin4. Или из командной строки:
# sudo -i -u postgres psql -d ltoolslogs -c 'CREATE EXTENSION pg_cron'Факт создания схемы cron и служебных объектов проверяется визуально.

2.2. Процедура автоматического создания секций
В БД ltoolslogs создаётся процедура, формирующая секции на следующий месяц для таблиц Logs и OrchEvents:
CREATE OR REPLACE PROCEDURE public.create_logs_part(
)
LANGUAGE 'plpgsql'
AS $BODY$
DECLARE
y integer;
m integer;
d1 timestamp;
d2 timestamp;
part varchar(100);
part_first varchar(100);
part_last varchar(100);
BEGIN
y := date_part('year', CURRENT_DATE);
m := date_part('month', CURRENT_DATE) + 1;
IF m > 12 THEN
y := y + 1;
m := 1;
END IF;
d1 = make_date(y, m, 1);
IF m = 12 THEN
d2 = make_date(y + 1, 1, 1);
ELSE
d2 = make_date(y, m + 1, 1);
END IF;
RAISE INFO 'Start checking and creating sections for [Logs] and [OrchEvents] from [%] to [%]...', d1, d2;
part := format('%s%s', y, CASE WHEN m > 9 THEN format('%s', m) ELSE format('0%s', m) END);
part_first := to_char(d1, 'yyyy-mm-dd');
part_last := to_char(d2, 'yyyy-mm-dd');
RAISE INFO 'Check if exists partition [Logs_%].', part;
if NOT EXISTS (
SELECT FROM pg_catalog.pg_class c
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
WHERE c.relname = format('Logs_%s', part)) then
RAISE INFO 'Creating [Logs_%] as partition of [Logs] from [%] to [%].', part, part_first, part_last;
EXECUTE format('create table "Logs_%s" partition of "Logs" for values from (''%s'') to (''%s'')', part, part_first, part_last);
EXECUTE format('CREATE INDEX "IX_Logs_%1$s_EntityId"
ON "Logs_%1$s" USING btree
("EntityId" COLLATE pg_catalog."default" ASC NULLS LAST)
TABLESPACE pg_default;
CREATE INDEX "IX_Logs_%1$s_Event"
ON public."Logs_%1$s" USING btree
("Event" ASC NULLS LAST)
TABLESPACE pg_default;
CREATE INDEX "IX_Logs_%1$s_Monitoring"
ON public."Logs_%1$s" USING btree
("RobotId" DESC NULLS LAST, "OrchTimestampUtc" DESC NULLS LAST, "LogType" ASC NULLS LAST, "Type" DESC NULLS LAST, "OperationKey" ASC NULLS LAST)
INCLUDE("RdpUserName")
TABLESPACE pg_default;
CREATE INDEX "IX_Logs_%1$s_Monitoring_1"
ON public."Logs_%1$s" USING btree
("Type" DESC NULLS LAST, "LogType" ASC NULLS LAST, "OperationKey" ASC NULLS LAST, "OrchTimestampUtc" DESC NULLS LAST)
TABLESPACE pg_default;
CREATE INDEX "IX_Logs_%1$s_Monitoring_2"
ON public."Logs_%1$s" USING btree
("OrchTimestampUtc" DESC NULLS LAST, "RobotId" DESC NULLS LAST, "LogType" ASC NULLS LAST)
TABLESPACE pg_default;
CREATE INDEX "IX_Logs_%1$s_Monitoring_3"
ON public."Logs_%1$s" USING btree
("LogType" ASC NULLS LAST, "OrchTimestampUtc" DESC NULLS LAST, "ProjectId" DESC NULLS LAST, "OperationKey" ASC NULLS LAST)
TABLESPACE pg_default;
CREATE INDEX "IX_Logs_%1$s_OperationKey"
ON "Logs_%1$s" USING btree
("OperationKey" ASC NULLS LAST)
TABLESPACE pg_default;
CREATE INDEX "IX_Logs_%1$s_ProjectId"
ON "Logs_%1$s" USING btree
("ProjectId" ASC NULLS LAST)
TABLESPACE pg_default;
CREATE INDEX "IX_Logs_%1$s_RobotId"
ON "Logs_%1$s" USING btree
("RobotId" ASC NULLS LAST)
TABLESPACE pg_default;
CREATE INDEX "IX_Logs_%1$s_RobotId_OperationKey_TimestampUtc"
ON "Logs_%1$s" USING btree
("RobotId" ASC NULLS LAST, "OperationKey" ASC NULLS LAST, "TimestampUtc" DESC NULLS LAST)
INCLUDE("Type", "LogType")
TABLESPACE pg_default;
CREATE INDEX "IX_Logs_%1$s_RobotKey"
ON "Logs_%1$s" USING btree
("RobotKey" COLLATE pg_catalog."default" ASC NULLS LAST)
TABLESPACE pg_default;
CREATE INDEX "IX_Logs_%1$s_UserId"
ON "Logs_%1$s" USING btree
("UserId" COLLATE pg_catalog."default" ASC NULLS LAST)
TABLESPACE pg_default;
CREATE INDEX "IX_Logs_%1$s_WorkerId"
ON "Logs_%1$s" USING btree
("WorkerId" ASC NULLS LAST)
TABLESPACE pg_default;', part);
else
RAISE INFO 'No new sections for [Logs] from [%] to [%] was created.', part_first, part_last;
end if;
RAISE INFO 'Check if exists partition [OrchEvents_%]...', part;
if NOT EXISTS (
SELECT FROM pg_catalog.pg_class c
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
WHERE c.relname = format('OrchEvents_%s', part)) then
RAISE INFO 'Creating section [OrchEvents_%] for table [OrchEvents] from [%] to [%].', part, part_first, part_last;
EXECUTE format('create table "OrchEvents_%s" partition of "OrchEvents" for values from (''%s'') to (''%s'')', part, part_first, part_last);
EXECUTE format('CREATE INDEX "IX_OrchEvents_%1$s_AllOrchTimestampUtc"
ON "OrchEvents_%1$s" USING btree
("TenantId" COLLATE pg_catalog."default" ASC NULLS LAST, "Event" ASC NULLS LAST, "UserId" COLLATE pg_catalog."default" ASC NULLS LAST, "EventType" ASC NULLS LAST, "OrchTimestampUtc" DESC NULLS LAST)
INCLUDE("EntityId", "OperationKey", "Signature", "WorkerAdminName", "Text", "IP", "AssignmentId", "LogType")
TABLESPACE pg_default;
CREATE INDEX "IX_OrchEvents_%1$s_EntityId_OrchTimestampUtc"
ON "OrchEvents_%1$s" USING btree
("EntityId" COLLATE pg_catalog."default" ASC NULLS LAST, "OrchTimestampUtc" ASC NULLS LAST)
TABLESPACE pg_default;
CREATE INDEX "IX_OrchEvents_%1$s_Event_OrchTimestampUtc"
ON "OrchEvents_%1$s" USING btree
("Event" ASC NULLS LAST, "OrchTimestampUtc" ASC NULLS LAST)
TABLESPACE pg_default;
CREATE INDEX "IX_OrchEvents_%1$s_UserId_OrchTimestampUtc"
ON "OrchEvents_%1$s" USING btree
("UserId" COLLATE pg_catalog."default" ASC NULLS LAST, "OrchTimestampUtc" ASC NULLS LAST)
TABLESPACE pg_default;', part);
else
RAISE INFO 'No new sections for [OrchEvents] from [%] to [%] was created.', part_first, part_last;
end if;
END;
$BODY$;2.3. Планирование задания
Создаётся задание pg_cron, которое будет еженедельно (в примере — каждое воскресенье в 01:00) вызывать процедуру. При необходимости расписание корректируется.
select cron.schedule('monthly-add-next-sections', '0 1 * * 0', 'CALL public.create_logs_part();');Для изменения задания достаточно удалить соответствующую запись из таблицы cron."job" и создать новую.

При необходимости процедуру можно выполнить вручную, минуя планировщик:
CALL public.create_logs_part();
Процедура всегда предварительно проверяет наличие секций на следующий месяц и создаёт их только при отсутствии.
3. Результат
После выполнения описанных действий таблицы Logs и OrchEvents становятся секционированными. Планировщик PostgreSQL использует фильтрацию секций (partition pruning) и индексы отдельных разделов, что позволяет значительно повысить производительность работы Оркестратора.
