Секционирование существующих таблиц с журналом Робота и Оркестратора для PostgreSQL
С общей информацией о секционировании вы можете ознакомиться на официальном сайте PostgreSQL .
Секционирование по месяцам
Для секционирования по месяцам нужно существующую таблицу подключить как раздел исторических данных.
Сначала для журнала робота.
Создаём новую головную секционированную таблицу идентичной таблице 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");Создаем секцию с данными на будущее (даты приведены для примера, замените их на актуальные). Можно создать сразу несколько секций, с запасом. Считаем, что начиная с даты ‘2026-02-16’ еще нет данных и секции будут помесячными:
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";и начать сначала.
Далее переименовываем таблицы и добавляем существующую таблицу (теперь она называется Logs_archive) как секцию к головной секционированной таблице Logs:
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 (в старую архивную таблицу). Начиная с ‘2026-02-16’ – в секцию Logs_20260216. Все запросы с фильтром по дате будут прозрачным для приложения образом идти через таблицу Logs.
Повторяем индексы на вновь созданной секции таблицы Logs_20260216 (если секций несколько, то для каждой секции):
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;Повторяем аналогичные действия по созданию секций и индексов для журнала Оркестратора.
Создаём новую головную секционированную таблицу идентичной таблице 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");Создаем секцию с данными на будущее (считаем, что начиная с даты ‘2026-02-16’ еще нет данных и секции будут помесячными):
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";Переименовываем таблицы и добавляем существующую таблицу (теперь она будет называться OrchEvents_archive) как секцию к головной секционированной таблице OrchEvents:
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_20260216:
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;Секций с новыми данными сразу можно добавить с запасом несколько. Дальнейшее их создание и удаление старых секций можно автоматизировать на основе задачи по расписанию.
Автоматическое создание новых секций
Устанавливаем расширение pg_cron (установка может отличаться для разных дистрибутивов Linux).
Копируем пакет с pg_cron в расшаренную папку, из которой будет производиться его установка. Переходим в эту папку:
# cd /srv/samba/shared/install/postgresql-13Устанавливаем пакет:
# dnf -y install 'pg_cron_13-1.3.0-1.rhel8.x86_64.rpm'Вносим изменения в файл postgresql.conf:
Устанавливаем (или дописываем) ‘pg_cron’ в значение для параметра shared_preload_libraries:
# vim /var/lib/pgsql/13/data/postgresql.conf
shared_preload_libraries = 'pg_cron'
Если еще не выставлено, то добавляем в секцию с настройками планировщика заданий флаг enable_partition_pruning:
enable_partition_pruning = on
Добавляем в конец файла строку:
cron.database_name = 'ltoolslogs'
Если требуется, чтобы cron-задания отрабатывали по локальному времени (по умолчанию выставлено GMT), то добавляем в качестве значения параметра cron.timezone свою временную зону, например:
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
Перезапускаем службу:
# systemctl restart postgresql-13Создаем в БД ltoolslogs расширение pg_cron. Можно через какой-то менеджер для работы с PostgreSQL, например, pgAdmin4. Или из командной строки:
# sudo -i -u postgres psql -d ltoolslogs -c 'CREATE EXTENSION pg_cron'После этого проверим, что в БД ltoolslogs появились схема cron и необходимые для заданий объекты БД:

В БД ltoolslogs создаем процедуру для создания секций на следующий месяц:
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$;В БД ltoolslogs создаем задание, которое будет каждое воскресенье в 1 час ночи (время и период срабатывания задания можно поменять на удобное вам) проверять наличие новых секций, и если их еще не существует, то создавать их для таблиц Logs и OrchEvents на следующий месяц, запуская созданную выше процедуру:
select cron.schedule('monthly-add-next-sections', '0 1 * * 0', 'CALL public.create_logs_part();');Для пересоздания задания нужно открыть таблицу cron.”job”, удалить запись с заданием и создать его заново:

Задание можно также запустить на выполнение вручную (без использования расширения pg_cron) такой командой:
CALL public.create_logs_part();
При этом задание также сначала проверит таблицы Logs и OrchEvents на наличие новых секций на следующий месяц, и при отсутствии создаст их.
После секционирования таблиц Logs и OrchEvents планировщик запросов будет строить планы запросов, использующие данные и индексы, относящиеся к отдельным секциям, что позволяет существенно повысить производительность работы Оркестратора:
