Секционирование существующих таблиц с журналом Робота и Оркестратора для SQLServer
1. Предварительные требования
1.1. Производящий описываемое в этом документе секционирование пользователь должен иметь права администратора на базе данных с логами оркестратора (по умолчанию она имеет наименование ltoolslogs).
1.2. Необходимо иметь запас свободного дискового пространства в размере 3–5 размеров текущей базы данных с логами оркестратора (ltoolslogs).
1.3. Во время выполнения описываемых процедур необходимо остановить сервис Primo.Orchestrator.RobotLogs.
1.4. В приводимых ниже примерах подразумевается, что текущая дата – «2026-02-25», и в названиях секции, файловой группы, функции и схемы секционирования применяется суффикс типа «_YYYYMMDD» (год, месяц, день). Замените эту дату на актуальную для вашей среды.
2. Создание файловой группы и файла для второй секции
2.1. Для секционирования по столбцу OrchTimestampUtc необходимо создать файловую группу для второй секции. Указывается дата, следующая за текущей (текущая дата + 1 день).
ALTER DATABASE ltoolslogs
ADD FILEGROUP ltoolslogs_group_20260226;
GO2.2. Подключите к файловой группе файл хранилища для второй секции (дата также соответствует дню, следующему за текущим). Путь к файлам .ndf зависит от конфигурации SQL Server (например, C:\Program Files\Microsoft SQL Server\MSSQL15.MSSQLSERVER\MSSQL\DATA).
ALTER DATABASE ltoolslogs
ADD FILE (
NAME = ltoolslogs_20260226,
FILENAME = '<file-groups-path>\ltoolslogs_20260226.ndf',
SIZE = 10 MB,
MAXSIZE = UNLIMITED,
FILEGROWTH = 1024 KB
) TO FILEGROUP ltoolslogs_group_20260226;
GO
3. Создание функции секционирования
3.1. Создайте начальную функцию секционирования (определяет границы диапазонов) для первых двух секций. Дата границы должна соответствовать дню, следующему за текущим.
CREATE PARTITION FUNCTION LogsPartitionFunction(datetime2(7))
AS RANGE RIGHT
FOR VALUES ('2026-02-26 00:00:00.0000000');
GO
3.2. Проверьте в SSMS, что функция секционирования создана.

4. Создание схемы секционирования
4.1. Создайте начальную схему секционирования (распределяет данные по файловым группам), использующую созданную функцию.
CREATE PARTITION SCHEME LogsPartitionScheme
AS PARTITION LogsPartitionFunction
TO ('Primary', 'ltoolslogs_group_20260226');
GO4.2. Проверьте в SSMS, что схема создана.

5. Настройка таблицы Logs
5.1. Удалите основной кластеризованный индекс.
ALTER TABLE [Logs]
DROP CONSTRAINT [PK_Logs];
GO5.2. Создайте такой же, но некластеризованный (с добавлением OrchTimestampUtc для уникальности в пределах секции).
ALTER TABLE [Logs] ADD CONSTRAINT [PK_Logs] PRIMARY KEY NONCLUSTERED
(
[Id] ASC,
[OrchTimestampUtc] ASC
)
ON [LogsPartitionScheme]([OrchTimestampUtc]);
GO5.3. Создайте кластеризованный индекс по столбцу секционирования.
CREATE CLUSTERED INDEX IX_Logs_OrchTimestampUtc
ON [Logs] ([OrchTimestampUtc] ASC)
ON [LogsPartitionScheme]([OrchTimestampUtc]);
GO5.4. Пересоздайте все некластеризованные индексы (каждый с DROP_EXISTING = ON и на схеме секционирования).
CREATE NONCLUSTERED INDEX [IX_Logs_EntityId]
ON [Logs] ([EntityId] ASC) WITH (DROP_EXISTING = ON)
ON [LogsPartitionScheme]([OrchTimestampUtc]);
GO
CREATE NONCLUSTERED INDEX [IX_Logs_Event]
ON [Logs] ([Event] ASC) WITH (DROP_EXISTING = ON)
ON [LogsPartitionScheme]([OrchTimestampUtc]);
GO
CREATE NONCLUSTERED INDEX [IX_Logs_Monitoring]
ON [Logs]
(
[RobotId] DESC,
[OrchTimestampUtc] DESC,
[LogType] ASC,
[Type] DESC,
[OperationKey] ASC
)
INCLUDE([RdpUserName]) WITH (DROP_EXISTING = ON)
ON [LogsPartitionScheme]([OrchTimestampUtc]);
GO
CREATE NONCLUSTERED INDEX [IX_Logs_Monitoring_1]
ON [Logs]
(
[Type] DESC,
[LogType] ASC,
[OperationKey] ASC,
[OrchTimestampUtc] DESC
) WITH (DROP_EXISTING = ON)
ON [LogsPartitionScheme]([OrchTimestampUtc]);
GO
CREATE NONCLUSTERED INDEX [IX_Logs_Monitoring_2]
ON [Logs]
(
[OrchTimestampUtc] DESC,
[RobotId] DESC,
[LogType] ASC
) WITH (DROP_EXISTING = ON)
ON [LogsPartitionScheme]([OrchTimestampUtc]);
GO
CREATE NONCLUSTERED INDEX [IX_Logs_Monitoring_3]
ON [Logs]
(
[LogType] ASC,
[OrchTimestampUtc] DESC,
[ProjectId] DESC,
[OperationKey] ASC
) WITH (DROP_EXISTING = ON)
ON [LogsPartitionScheme]([OrchTimestampUtc]);
GO
CREATE NONCLUSTERED INDEX [IX_Logs_OperationKey]
ON [Logs] ([OperationKey] ASC) WITH (DROP_EXISTING = ON)
ON [LogsPartitionScheme]([OrchTimestampUtc]);
GO
CREATE NONCLUSTERED INDEX [IX_Logs_ProjectId]
ON [Logs] ([ProjectId] ASC) WITH (DROP_EXISTING = ON)
ON [LogsPartitionScheme]([OrchTimestampUtc]);
GO
CREATE NONCLUSTERED INDEX [IX_Logs_RobotId]
ON [Logs] ([RobotId] ASC) WITH (DROP_EXISTING = ON)
ON [LogsPartitionScheme]([OrchTimestampUtc]);
GO
CREATE NONCLUSTERED INDEX [IX_Logs_RobotId_OperationKey_TimestampUtc]
ON [Logs]
(
[RobotId] ASC,
[OperationKey] ASC,
[TimestampUtc] DESC
)
INCLUDE([Type],[LogType]) WITH (DROP_EXISTING = ON)
ON [LogsPartitionScheme]([OrchTimestampUtc]);
GO
CREATE NONCLUSTERED INDEX [IX_Logs_RobotKey]
ON [Logs] ([RobotKey] ASC) WITH (DROP_EXISTING = ON)
ON [LogsPartitionScheme]([OrchTimestampUtc]);
GO
CREATE NONCLUSTERED INDEX [IX_Logs_UserId]
ON [Logs] ([UserId] ASC) WITH (DROP_EXISTING = ON)
ON [LogsPartitionScheme]([OrchTimestampUtc]);
GO
CREATE NONCLUSTERED INDEX [IX_Logs_WorkerId]
ON [Logs] ([WorkerId] ASC) WITH (DROP_EXISTING = ON)
ON [LogsPartitionScheme]([OrchTimestampUtc]);
GO6. Настройка таблицы OrchEvents
6.1. Удалите основной кластеризованный индекс.
ALTER TABLE [OrchEvents]
DROP CONSTRAINT [PK_OrchEvents];
GO6.2. Создайте такой же, но некластеризованный.
ALTER TABLE [OrchEvents] ADD CONSTRAINT [PK_OrchEvents] PRIMARY KEY NONCLUSTERED
(
[Id] ASC,
[OrchTimestampUtc] ASC
)
ON [LogsPartitionScheme]([OrchTimestampUtc]);
GO6.3. Создайте кластеризованный индекс для секционирования.
CREATE CLUSTERED INDEX IX_OrchEvents_OrchTimestampUtc
ON [OrchEvents] ([OrchTimestampUtc])
ON [LogsPartitionScheme]([OrchTimestampUtc]);
GO6.4. Пересоздайте некластеризованные индексы.
CREATE NONCLUSTERED INDEX [IX_OrchEvents_AllOrchTimestampUtc]
ON [OrchEvents]
(
[TenantId] ASC,
[Event] ASC,
[UserId] ASC,
[EventType] ASC,
[OrchTimestampUtc] DESC
)
INCLUDE([EntityId],[OperationKey],[Signature],[WorkerAdminName],[Text],[IP],[AssignmentId],[LogType])
WITH (DROP_EXISTING = ON)
ON [LogsPartitionScheme]([OrchTimestampUtc]);
GO
CREATE NONCLUSTERED INDEX [IX_OrchEvents_EntityId_OrchTimestampUtc]
ON [OrchEvents]
(
[EntityId] ASC,
[OrchTimestampUtc] ASC
) WITH (DROP_EXISTING = ON)
ON [LogsPartitionScheme]([OrchTimestampUtc]);
GO
CREATE NONCLUSTERED INDEX [IX_OrchEvents_Event_OrchTimestampUtc]
ON [OrchEvents]
(
[Event] ASC,
[OrchTimestampUtc] ASC
) WITH (DROP_EXISTING = ON)
ON [LogsPartitionScheme]([OrchTimestampUtc]);
GO
CREATE NONCLUSTERED INDEX [IX_OrchEvents_UserId_OrchTimestampUtc]
ON [OrchEvents]
(
[UserId] ASC,
[OrchTimestampUtc] ASC
) WITH (DROP_EXISTING = ON)
ON [LogsPartitionScheme]([OrchTimestampUtc]);
GO7. Хранимая процедура по созданию новых секций
7.1. Создайте в базе данных ltoolslogs хранимую процедуру create_partition, выполнив следующий скрипт.
CREATE PROCEDURE dbo.create_partition
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from interfering with SELECT statements.
SET NOCOUNT ON;
-- Ensures all errors, including those with lower severity, cause a transaction rollback
SET XACT_ABORT ON;
DECLARE @DbName sysname = DB_NAME();
DECLARE @CurrentDate DATE = GETDATE();
DECLARE @NewBoundaryDate DATE; -- next day
DECLARE @BoundaryValue NVARCHAR(24); -- boundary value for split partition function
DECLARE @Suffix NVARCHAR(10); -- suffix for file/fule group/section
DECLARE @FilegroupName sysname;
DECLARE @FileName NVARCHAR(511);
DECLARE @FileNamePath NVARCHAR(511);
DECLARE @FileExists INT;
DECLARE @functionName AS NVARCHAR(128) = N'LogsPartitionFunction';
DECLARE @SchemeName AS NVARCHAR(128) = N'LogsPartitionScheme';
-- Optional: Log error details or re-throw the error
DECLARE @ErrorMessage NVARCHAR(4000);
DECLARE @ErrorSeverity INT = 16;
DECLARE @ErrorState INT = 1;
-- Declare a variable for the dynamic SQL command
DECLARE @Sql NVARCHAR(MAX);
---- Создание файловой группы и файла с данными и индексами секции
-- Get next Date and suffix for it
SET @NewBoundaryDate = DATEADD(DAY, 1, @CurrentDate);
SET @BoundaryValue = CONCAT(YEAR(@NewBoundaryDate), N'-', FORMAT(MONTH(@NewBoundaryDate), '00'), N'-', FORMAT(DAY(@NewBoundaryDate), '00'), N'T00:00:00.000');
PRINT N'Boundary for next date [' + CAST(@NewBoundaryDate as VARCHAR(20)) + N'] = ' + @BoundaryValue;
SET @Suffix = CONCAT(YEAR(@NewBoundaryDate), FORMAT(MONTH(@NewBoundaryDate), '00'), FORMAT(DAY(@NewBoundaryDate), '00'));
PRINT N'Suffix for next date [' + CAST(@NewBoundaryDate as VARCHAR(20)) + N'] = ' + @Suffix;
-- Set FileGroupName and FileName for new section
SET @FileGroupName = CONCAT(N'ltoolslogs_group_', @Suffix);
SET @FileName = CONCAT(N'ltoolslogs_', @Suffix);
-- Check and add filegroup for current section
PRINT CHAR(13) + CHAR(10) + N'Check if exists filegroup [' + @FileGroupName + N']...';
IF NOT EXISTS (SELECT * FROM sys.filegroups WHERE name = @FileGroupName)
BEGIN
PRINT N'Filegroup [' + @FileGroupName + N'] was not found. Creating it.';
BEGIN TRY
-- Construct the dynamic SQL statement
SET @Sql = N'ALTER DATABASE CURRENT ADD FILEGROUP [' + @FileGroupName + N'];';
-- Print the command for verification (optional)
PRINT @Sql;
-- Execute the dynamic SQL command
EXEC sp_executesql @Sql;
PRINT N'Filegroup [' + @FileGroupName + N'] was added for database "' + @DbName + '"';
END TRY
BEGIN CATCH
-- Optional: Log error details or re-throw the error
SET @ErrorMessage = ERROR_MESSAGE();
SET @ErrorSeverity = ERROR_SEVERITY();
SET @ErrorState = ERROR_STATE();
THROW @ErrorMessage, @ErrorSeverity, @ErrorState;
END CATCH;
END
ELSE
BEGIN
PRINT N'Filegroup [' + @FileGroupName + N'] is exists already.';
END
-- Looking for Current File Name
SET @FileNamePath = CAST(SERVERPROPERTY('InstanceDefaultDataPath') AS NVARCHAR(511)) + @FileName + N'.ndf';
-- Check and add filegroup for current section
PRINT CHAR(13) + CHAR(10) + N'Check if exists file [' + @FileNamePath + N']...';
-- Execute the extended stored procedure, providing the output variable
EXEC master.dbo.xp_fileexist @FileNamePath, @FileExists OUTPUT;
-- Check the output parameter in an IF NOT EXISTS structure
IF (SELECT @FileExists) = 0
BEGIN
PRINT N'File [' + @FileNamePath + N'] was not found. Creating it.';
BEGIN TRY
-- Construct and execute dynamic SQL to add the file to the filegroup
SET @Sql = N'ALTER DATABASE CURRENT ADD FILE
(
NAME = N''' + @FileName + N''',
FILENAME = N''' + @FileNamePath + N''',
SIZE = 10240KB,
FILEGROWTH = 10240KB
) TO FILEGROUP [' + @FileGroupName + N'];';
-- Print the command for verification (optional)
PRINT @Sql;
EXEC sp_executesql @Sql;
PRINT N'File [' + @FileNamePath + N'] was created and added to [' + @FileGroupName + N'] filegroup.';
END TRY
BEGIN CATCH
-- Optional: Log error details or re-throw the error
SET @ErrorMessage = ERROR_MESSAGE();
SET @ErrorSeverity = ERROR_SEVERITY();
SET @ErrorState = ERROR_STATE();
THROW @ErrorMessage, @ErrorSeverity, @ErrorState;
END CATCH;
END
ELSE
BEGIN
PRINT N'File [' + @FileNamePath + N'] is exists already.';
END
---- Изменение схемы и функции секционирования
PRINT CHAR(13) + CHAR(10) + N'Check if exists partition scheme [' + @SchemeName + N'] and function ['+ @functionName + N']...';
-- Check if the partition scheme does not exist
IF NOT EXISTS (SELECT name FROM sys.partition_schemes WHERE name = @SchemeName)
BEGIN
SET @ErrorMessage = N'Partition scheme [' + @SchemeName + N'] does not exist. Throw and skip job.';
PRINT @ErrorMessage;
RAISERROR(@ErrorMessage, @ErrorSeverity, @ErrorState);
END
ELSE
-- Check if the partition function does not exist
IF NOT EXISTS (SELECT name FROM sys.partition_functions WHERE name = @functionName)
BEGIN
SET @ErrorMessage = N'Partition function [' + @functionName + N'] does not exist. Throw and skip job.';
PRINT @ErrorMessage;
RAISERROR(@ErrorMessage, @ErrorSeverity, @ErrorState);
END
ELSE
BEGIN
BEGIN TRY
-- 1. Ensure a filegroup is marked as NEXT USED in the partition scheme
PRINT N'Altering partition scheme [' + @SchemeName + N']...';
SET @Sql = N'ALTER PARTITION SCHEME ' + QUOTENAME(@SchemeName) + N' NEXT USED ' + QUOTENAME(@FilegroupName) + N';';
-- Print the command for verification (optional)
PRINT @Sql;
EXEC sp_executesql @Sql;
-- 2. Split the partition function at the new boundary value
PRINT N'Altering partition function [' + @functionName + N']...';
SET @Sql = N'ALTER PARTITION FUNCTION ' + QUOTENAME(@functionName) + N'() SPLIT RANGE (' + N'''' + CAST(@BoundaryValue AS VARCHAR(24)) + '''' + N');';
-- Print the command for verification (optional)
PRINT @Sql;
EXEC sp_executesql @Sql;
PRINT N'Partition function [' + @functionName + N'] altered successfully.';
END TRY
BEGIN CATCH
-- Optional: Log error details or re-throw the error
SET @ErrorMessage = ERROR_MESSAGE();
SET @ErrorSeverity = ERROR_SEVERITY();
SET @ErrorState = ERROR_STATE();
THROW @ErrorMessage, @ErrorSeverity, @ErrorState;
END CATCH;
END;
END;7.2. Указанная хранимая процедура при её выполнении создаёт новую секцию (добавляются файловая группа и файл, изменяются функция и схема секционирования), которая будет содержать данные, начиная со дня, следующего за текущим. Процедуру можно запускать как вручную, так и через автоматическое задание Агента SQL Server.
8. Создание новой секции вручную
8.1. Для создания новой секции (для данных следующего дня) выполните команду:
EXEC dbo.create_partition;
8.2. Пример: если текущая дата – «2026-03-02», то при запуске create_partition будут произведены следующие изменения:
-
создана файловая группа
ltoolslogs_group_20260303;
-
создан файл
ltoolslogs_20260303.ndfдля хранения данных и индексов этой секции;
-
функция и схема секционирования дополнены новой секцией с границей
2026-03-03 00:00:00.0000000.
-
открытие свойств БД ltoolslogs в SSMS показывает, что файл группа и файл под новую секцию успешно созданы.

8.3. Далее по мере необходимости (раз в несколько дней, недель или месяцев) необходимо повторно запускать процедуру
create_partitionдля создания очередной секции.
9. Автоматическое создание новых секций с помощью Агента SQL Server
9.1. Служба SQL Server Agent входит в стандартную поставку SQL Server. Для её работы необходимо в оснастке «Службы» установить автоматический запуск этой службы и запустить её.


9.2. Первый способ создания задания – с помощью скрипта. В примере ниже задание настроено на еженедельный запуск по субботам в 13:10.
USE msdb;
GO
EXECUTE dbo.sp_add_job @job_name = N'Create partition at ltoolslogs';
GO
EXECUTE sp_add_jobstep
@job_name = N'Create partition at ltoolslogs',
@step_name = N'Execute procedure create_partition',
@subsystem = N'TSQL',
@command = N'EXEC dbo.create_partition',
@database_name = N'ltoolslogs',
@retry_attempts = 5,
@retry_interval = 5;
GO
EXECUTE dbo.sp_add_schedule
@schedule_name = N'RunWeekly',
@freq_type = 8, -- еженедельное расписание
@freq_interval = 64, -- выполнение по субботам
@freq_subday_type = 1, -- точное время
@freq_relative_interval = 0,
@freq_recurrence_factor = 1,
@active_start_time = 131000, -- время начала 13:10:00
@active_end_time = 233000; -- время окончания 23:30:00
GO
EXECUTE sp_attach_schedule
@job_name = N'Create partition at ltoolslogs',
@schedule_name = N'RunWeekly';
GO
EXECUTE dbo.sp_add_jobserver @job_name = N'Create partition at ltoolslogs';
GO9.3. Второй способ – использование графического интерфейса SSMS:
-
На вкладке «Задания» для Агента SQL Server щёлкните правой кнопкой мыши и выберите «Создать задание».

-
На вкладке «Общие» укажите имя задания (например,
Create partition at ltoolslogs).
-
На вкладке «Шаги» создайте шаг с типом «Скрипт Transact-SQL (T-SQL)», выберите базу данных
ltoolslogsи введите командуEXEC dbo.create_partition;.
-
На вкладке «Расписания» создайте расписание (например, еженедельное по воскресеньям в 17:10) с установленным флагом «Включено».
-
Нажмите «ОК» – задание будет создано и активировано.

9.4. Для просмотра журнала выполнения задания в обозревателе объектов SSMS щёлкните правой кнопкой мыши на названии задания и выберите «Просмотр журнала».

9.5. После создания новой секции можно открыть свойства базы данных
ltoolslogsв SSMS и убедиться, что файл и файловая группа для новой секции созданы.
10. Очистка секций от ненужных данных
10.1. Для очистки секций таблиц Logs и OrchEvents от устаревших данных (например, для секции, содержащей данные с OrchTimestampUtc = '2026-03-03') используйте следующий скрипт:
DECLARE @p INT = $PARTITION.LogsPartitionFunction('2026-03-03');
TRUNCATE TABLE Logs WITH (PARTITIONS(@p));
TRUNCATE TABLE OrchEvents WITH (PARTITIONS(@p));10.2. Для предварительной проверки количества записей в целевой секции выполните:
SELECT @p AS [partition number], COUNT(*) AS [Logs rows] FROM dbo.Logs
WHERE $PARTITION.LogsPartitionFunction(OrchTimestampUtc) = @p;
SELECT @p AS [partition number], COUNT(*) AS [OrchEvents rows] FROM dbo.OrchEvents
WHERE $PARTITION.LogsPartitionFunction(OrchTimestampUtc) = @p;
Примечание: Все примеры используют текущую дату
2026-02-25и дату следующего дня2026-02-26. Перед выполнением замените их на значения, актуальные для вашей среды.