From 9f1baf17ac4a7c84862623e291b88590bdde86e4 Mon Sep 17 00:00:00 2001 From: waka Date: Thu, 28 May 2026 13:27:31 +0500 Subject: [PATCH] initial --- .editorconfig | 10 + .gitattributes | 5 + .gitignore | 17 + README.md | 14 + deploy_legacy.txt | 1595 +++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 1641 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 README.md create mode 100644 deploy_legacy.txt diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..7fe9e0b --- /dev/null +++ b/.editorconfig @@ -0,0 +1,10 @@ +root = true + +[*] +charset = utf-8 +end_of_line = crlf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..de31bec --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +* text=auto + +*.sql text working-tree-encoding=UTF-8 +*.txt text working-tree-encoding=UTF-8 +*.md text working-tree-encoding=UTF-8 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..03ad17a --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +# SQL Server / tooling output +*.bak +*.trn +*.xel +*.trc +*.log + +# Local scratch files +tmp/ +temp/ +*.tmp + +# Editor / OS noise +.vs/ +.vscode/ +Thumbs.db +Desktop.ini diff --git a/README.md b/README.md new file mode 100644 index 0000000..669de7f --- /dev/null +++ b/README.md @@ -0,0 +1,14 @@ +# mssql-maintenance-deploy + +Проект для разработки идемпотентного развертывания инфраструктуры обслуживания баз MS SQL Server. + +## Текущий статус + +- `deploy_legacy.txt` - исходный legacy-скрипт для анализа. +- Новый deploy-пайплайн пока не реализован. + +## Ближайшие шаги + +1. Зафиксировать требования к установке и обновлению. +2. Разделить legacy на bootstrap, schema, programmable objects, seed/config и SQL Agent jobs. +3. Сделать безопасный повторный запуск без потери истории и настроек. diff --git a/deploy_legacy.txt b/deploy_legacy.txt new file mode 100644 index 0000000..f2bc26f --- /dev/null +++ b/deploy_legacy.txt @@ -0,0 +1,1595 @@ +USE [DBA]; +GO + +/*----------------------------------------------------------- + Удаляем таблицы и процедуры, если они есть +------------------------------------------------------------*/ + +DROP TABLE IF EXISTS dbo.MaintenanceLog; +GO + +DROP TABLE IF EXISTS dbo.IndexMaintenanceTask; +GO + +DROP TABLE IF EXISTS dbo.StatsMaintenanceTask; +GO + +DROP TABLE IF EXISTS dbo.MaintenanceConfig; +GO + +DROP TABLE IF EXISTS dbo.MaintenanceRun; +GO + +DROP PROCEDURE IF EXISTS dbo.usp_IndexCandidateSelect; +GO + +DROP PROCEDURE IF EXISTS dbo.usp_IndexDefrag_Process; +GO + +DROP PROCEDURE IF EXISTS dbo.usp_StatsCandidateSelect; +GO + +DROP PROCEDURE IF EXISTS dbo.usp_StatsUpdate_Process; +GO + +DROP PROCEDURE IF EXISTS dbo.usp_RunMaintenance; +GO + +/*----------------------------------------------------------- + Создаем таблицы +------------------------------------------------------------*/ + +CREATE TABLE dbo.MaintenanceRun +( + RunId bigint IDENTITY(1,1) NOT NULL CONSTRAINT PK_MaintenanceRun PRIMARY KEY, + ServerName sysname NOT NULL CONSTRAINT DF_MaintenanceRun_ServerName DEFAULT @@SERVERNAME, + DatabaseName sysname NOT NULL, + JobName sysname NULL, + Initiator varchar(20) NOT NULL CONSTRAINT DF_MaintenanceRun_Initiator DEFAULT ('SQLAGENT'), + SessionId int NULL CONSTRAINT DF_MaintenanceRun_SessionId DEFAULT (@@SPID), + StartTime datetime2(0) NOT NULL CONSTRAINT DF_MaintenanceRun_StartTime DEFAULT (SYSDATETIME()), + EndTime datetime2(0) NULL, + Status varchar(20) NOT NULL CONSTRAINT DF_MaintenanceRun_Status DEFAULT ('RUNNING'), + MinPages int NULL, + MinFrag decimal(10,2) NULL, + MaxDop int NULL, + MinRows bigint NULL, + MinModPct decimal(10,2) NULL, + MinModAbs bigint NULL, + SamplePercent int NULL, + LockTimeoutMs int NULL, + CutoffTime datetime2(0) NULL, + IndexCandidates int NULL, + IndexProcessed int NULL, + IndexErrors int NULL, + IndexLockTimeouts int NULL, + IndexDeadlocks int NULL, + IndexTimeLimitSkipped int NULL, + StatsCandidates int NULL, + StatsProcessed int NULL, + StatsErrors int NULL, + ErrorMessage nvarchar(max) NULL +); +GO + +CREATE INDEX IX_MaintenanceRun_DatabaseName_StartTime ON dbo.MaintenanceRun(DatabaseName, StartTime DESC); +GO + +CREATE INDEX IX_MaintenanceRun_Status_StartTime ON dbo.MaintenanceRun(Status, StartTime DESC); +GO + +CREATE TABLE dbo.IndexMaintenanceTask +( + TaskId bigint IDENTITY(1,1) NOT NULL CONSTRAINT PK_IndexMaintenanceTask PRIMARY KEY, + RunId bigint NOT NULL, + ServerName sysname NOT NULL CONSTRAINT DF_IndexMaintenanceTask_ServerName DEFAULT @@SERVERNAME, + DatabaseName sysname NOT NULL, + SchemaName sysname NOT NULL, + TableName sysname NOT NULL, + IndexName sysname NOT NULL, + ObjectId int NOT NULL, + IndexId int NOT NULL, + IndexType nvarchar(60) NULL, + PageCount bigint NULL, + FragPercent decimal(12,3) NULL, + ScanMode varchar(20) NOT NULL, + OperationType varchar(20) NOT NULL CONSTRAINT DF_IndexMaintenanceTask_OperationType DEFAULT ('REBUILD'), + AttemptNo int NOT NULL CONSTRAINT DF_IndexMaintenanceTask_AttemptNo DEFAULT (1), + CommandText nvarchar(max) NULL, + Status varchar(20) NOT NULL CONSTRAINT DF_IndexMaintenanceTask_Status DEFAULT ('PENDING'), + SkipReason varchar(30) NULL, + QueueTime datetime2(0) NOT NULL CONSTRAINT DF_IndexMaintenanceTask_QueueTime DEFAULT (SYSDATETIME()), + StartTime datetime2(0) NULL, + EndTime datetime2(0) NULL, + DurationMin decimal(10,2) NULL, + ErrorNumber int NULL, + ErrorMessage nvarchar(max) NULL +); +GO + +ALTER TABLE dbo.IndexMaintenanceTask +ADD CONSTRAINT FK_IndexMaintenanceTask_MaintenanceRun + FOREIGN KEY (RunId) + REFERENCES dbo.MaintenanceRun(RunId); +GO + +CREATE INDEX IX_IndexMaintenanceTask_RunId_Status ON dbo.IndexMaintenanceTask(RunId, Status); +GO + +CREATE INDEX IX_IndexMaintenanceTask_Db_Object ON dbo.IndexMaintenanceTask(DatabaseName, ObjectId, IndexId); +GO + +CREATE INDEX IX_IndexMaintenanceTask_Status_QueueTime ON dbo.IndexMaintenanceTask(Status, QueueTime); +GO + +CREATE TABLE dbo.StatsMaintenanceTask +( + TaskId bigint IDENTITY(1,1) NOT NULL CONSTRAINT PK_StatsMaintenanceTask PRIMARY KEY, + RunId bigint NOT NULL, + ServerName sysname NOT NULL CONSTRAINT DF_StatsMaintenanceTask_ServerName DEFAULT @@SERVERNAME, + DatabaseName sysname NOT NULL, + SchemaName sysname NOT NULL, + TableName sysname NOT NULL, + ObjectId int NULL, + StatsName sysname NULL, + RowsCount bigint NULL, + ModificationCounter bigint NULL, + ModPct decimal(8,3) NULL, + UpdateMode varchar(20) NOT NULL, + SamplePercent int NULL, + CommandText nvarchar(max) NULL, + Status varchar(20) NOT NULL CONSTRAINT DF_StatsMaintenanceTask_Status DEFAULT ('PENDING'), + QueueTime datetime2(0) NOT NULL CONSTRAINT DF_StatsMaintenanceTask_QueueTime DEFAULT (SYSDATETIME()), + StartTime datetime2(0) NULL, + EndTime datetime2(0) NULL, + DurationMin decimal(10,2) NULL, + ErrorNumber int NULL, + ErrorMessage nvarchar(max) NULL +); +GO + +ALTER TABLE dbo.StatsMaintenanceTask +ADD CONSTRAINT FK_StatsMaintenanceTask_MaintenanceRun + FOREIGN KEY (RunId) + REFERENCES dbo.MaintenanceRun(RunId); +GO + +CREATE INDEX IX_StatsMaintenanceTask_RunId_Status ON dbo.StatsMaintenanceTask(RunId, Status); +GO + +CREATE INDEX IX_StatsMaintenanceTask_Db_Object ON dbo.StatsMaintenanceTask(DatabaseName, ObjectId); +GO + +CREATE INDEX IX_StatsMaintenanceTask_Status_QueueTime ON dbo.StatsMaintenanceTask(Status, QueueTime); +GO + +CREATE TABLE dbo.MaintenanceLog +( + LogId bigint IDENTITY(1,1) NOT NULL CONSTRAINT PK_MaintenanceLog PRIMARY KEY, + RunId bigint NOT NULL, + TaskType varchar(20) NULL, + TaskId bigint NULL, + LogTime datetime2(0) NOT NULL CONSTRAINT DF_MaintenanceLog_LogTime DEFAULT (SYSDATETIME()), + LogLevel varchar(10) NOT NULL, + StepName varchar(100) NULL, + Message nvarchar(max) NOT NULL +); +GO + +ALTER TABLE dbo.MaintenanceLog +ADD CONSTRAINT FK_MaintenanceLog_MaintenanceRun + FOREIGN KEY (RunId) + REFERENCES dbo.MaintenanceRun(RunId); +GO + +CREATE INDEX IX_MaintenanceLog_RunId_LogTime ON dbo.MaintenanceLog(RunId, LogTime); +GO + +CREATE INDEX IX_MaintenanceLog_TaskType_TaskId ON dbo.MaintenanceLog(TaskType, TaskId); +GO + +CREATE INDEX IX_MaintenanceLog_LogLevel_LogTime ON dbo.MaintenanceLog(LogLevel, LogTime DESC); +GO + +CREATE TABLE dbo.MaintenanceConfig +( + ConfigId int IDENTITY(1,1) NOT NULL CONSTRAINT PK_MaintenanceConfig PRIMARY KEY, + ServerName sysname NOT NULL CONSTRAINT DF_MaintenanceConfig_ServerName DEFAULT @@SERVERNAME, + DatabaseName sysname NOT NULL, + IsEnabled bit NOT NULL CONSTRAINT DF_MaintenanceConfig_IsEnabled DEFAULT (1), + MinPages int NOT NULL CONSTRAINT DF_MaintenanceConfig_MinPages DEFAULT (1000), + MinFrag decimal(5,2) NOT NULL CONSTRAINT DF_MaintenanceConfig_MinFrag DEFAULT (5.00), + MaxDop int NOT NULL CONSTRAINT DF_MaintenanceConfig_MaxDop DEFAULT (4), + MinRows bigint NOT NULL CONSTRAINT DF_MaintenanceConfig_MinRows DEFAULT (5000), + MinModPct decimal(5,2) NOT NULL CONSTRAINT DF_MaintenanceConfig_MinModPct DEFAULT (10.00), + MinModAbs bigint NOT NULL CONSTRAINT DF_MaintenanceConfig_MinModAbs DEFAULT (5000), + LockTimeoutMs int NOT NULL CONSTRAINT DF_MaintenanceConfig_LockTimeoutMs DEFAULT (5000), + RebuildCutoffTime time(0) NULL, + ExcludeSchemas nvarchar(max) NULL, + ExcludeTables nvarchar(max) NULL, + Notes nvarchar(1000) NULL, + ModifiedAt datetime2(0) NOT NULL CONSTRAINT DF_MaintenanceConfig_ModifiedAt DEFAULT (SYSDATETIME()) +); +GO + +CREATE UNIQUE INDEX UX_MaintenanceConfig_DatabaseName ON dbo.MaintenanceConfig(DatabaseName); +GO + +/*----------------------------------------------------------- + Записываем начальные конфиги для всех баз +------------------------------------------------------------*/ + +SET NOCOUNT ON; +SET XACT_ABORT ON; +GO + +DECLARE @IsEnabled bit = 1; +DECLARE @MinPages int = 1000; +DECLARE @MinFrag decimal(5,2) = 5.00; +DECLARE @MaxDop int = 4; +DECLARE @MinRows bigint = 5000; +DECLARE @MinModPct decimal(5,2) = 5.00; +DECLARE @MinModAbs bigint = 40000; +DECLARE @LockTimeoutMs int = 5000; +DECLARE @RebuildCutoffTime time(0) = '06:00:00'; +DECLARE @Notes nvarchar(1000) = N'Инициализация'; + +INSERT INTO dbo.MaintenanceConfig +( + DatabaseName, + IsEnabled, + MinPages, + MinFrag, + MaxDop, + MinRows, + MinModPct, + MinModAbs, + LockTimeoutMs, + RebuildCutoffTime, + Notes, + ModifiedAt +) +SELECT + d.name, + @IsEnabled, + @MinPages, + @MinFrag, + @MaxDop, + @MinRows, + @MinModPct, + @MinModAbs, + @LockTimeoutMs, + @RebuildCutoffTime, + @Notes, + SYSDATETIME() +FROM sys.databases AS d +WHERE d.database_id > 4 + AND d.name != N'DBA' + AND d.state_desc = 'ONLINE' + AND d.source_database_id IS NULL + AND NOT EXISTS + ( + SELECT 1 + FROM dbo.MaintenanceConfig AS c + WHERE c.DatabaseName = d.name + ); +GO + +/*----------------------------------------------------------- + Определяем хранимые процедуры +------------------------------------------------------------*/ + +CREATE OR ALTER PROCEDURE dbo.usp_Index_SelectCandidates +( + @DatabaseName sysname, + @RunId bigint, + @PhaseName varchar(20) +) +AS +BEGIN + SET NOCOUNT ON; + SET XACT_ABORT ON; + + DECLARE + @MinPages int, + @MinFrag decimal(5,2), + @IsEnabled bit, + @Cmd nvarchar(max), + @Inserted int = 0, + @AdminDb sysname = DB_NAME(); + + SET @PhaseName = UPPER(@PhaseName); + + IF @PhaseName NOT IN ('LIMITED', 'SAMPLED') + THROW 50015, N'@PhaseName должен быть LIMITED или SAMPLED', 1; + + IF @DatabaseName IS NULL OR LTRIM(RTRIM(@DatabaseName)) = N'' + THROW 50001, N'usp_Index_SelectCandidates: не задан параметр @DatabaseName', 1; + + IF @RunId IS NULL + THROW 50002, N'usp_Index_SelectCandidates: не задан параметр @RunId', 1; + + IF DB_ID(@DatabaseName) IS NULL + THROW 50003, N'usp_Index_SelectCandidates: указанная база не существует', 1; + + SELECT + @IsEnabled = IsEnabled, + @MinPages = MinPages, + @MinFrag = MinFrag + FROM dbo.MaintenanceConfig + WHERE DatabaseName = @DatabaseName; + + IF @IsEnabled IS NULL + THROW 50004, N'usp_Index_SelectCandidates: для базы отсутствует запись в dbo.MaintenanceConfig', 1; + + IF @IsEnabled = 0 + BEGIN + INSERT INTO dbo.MaintenanceLog + ( + RunId, + TaskType, + TaskId, + LogLevel, + StepName, + Message + ) + VALUES + ( + @RunId, + 'RUN', + NULL, + 'WARN', + 'INDEX_SELECT_' + @PhaseName, + N'Этап пропущен: обслуживание для базы отключено в MaintenanceConfig.' + ); + + RETURN; + END; + + INSERT INTO dbo.MaintenanceLog + ( + RunId, + TaskType, + TaskId, + LogLevel, + StepName, + Message + ) + VALUES + ( + @RunId, + 'RUN', + NULL, + 'INFO', + 'INDEX_SELECT_' + @PhaseName, + N'Начат отбор кандидатов в режиме ' + @PhaseName + ); + + SET @Cmd = N' + INSERT INTO ' + QUOTENAME(@AdminDb) + N'.dbo.IndexMaintenanceTask + ( + RunId, + DatabaseName, + SchemaName, + TableName, + IndexName, + ObjectId, + IndexId, + IndexType, + PageCount, + FragPercent, + ScanMode, + OperationType, + Status, + CommandText + ) + SELECT + @p_RunId, + @p_DatabaseName, + s.name, + o.name, + i.name, + i.object_id, + i.index_id, + i.type_desc, + ips.page_count, + ips.avg_fragmentation_in_percent, + @p_ScanMode, + ''REBUILD'', + ''PENDING'', + NULL + FROM sys.dm_db_index_physical_stats(DB_ID(@p_DatabaseName), NULL, NULL, NULL, @p_ScanMode) AS ips + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.indexes AS i + ON i.object_id = ips.object_id + AND i.index_id = ips.index_id + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.objects AS o + ON o.object_id = i.object_id + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.schemas AS s + ON s.schema_id = o.schema_id + WHERE + o.type = ''U'' + AND ips.alloc_unit_type_desc = ''IN_ROW_DATA'' + AND ips.index_id > 0 + AND ips.page_count >= @p_MinPages + AND ips.avg_fragmentation_in_percent >= @p_MinFrag + AND i.is_disabled = 0 + AND i.is_hypothetical = 0 + AND i.type_desc IN (''CLUSTERED'', ''NONCLUSTERED'') + AND NOT EXISTS + ( + SELECT 1 + FROM ' + QUOTENAME(@AdminDb) + N'.dbo.IndexMaintenanceTask AS t + WHERE t.RunId = @p_RunId + AND t.DatabaseName = @p_DatabaseName + AND t.ObjectId = i.object_id + AND t.IndexId = i.index_id + ); + + SET @p_Inserted = @@ROWCOUNT; + '; + + EXEC sys.sp_executesql + @Cmd, + N'@p_RunId bigint, + @p_DatabaseName sysname, + @p_ScanMode varchar(20), + @p_MinPages int, + @p_MinFrag decimal(5,2), + @p_Inserted int OUTPUT', + @p_RunId = @RunId, + @p_DatabaseName = @DatabaseName, + @p_ScanMode = @PhaseName, + @p_MinPages = @MinPages, + @p_MinFrag = @MinFrag, + @p_Inserted = @Inserted OUTPUT; + + INSERT INTO dbo.MaintenanceLog + ( + RunId, + TaskType, + TaskId, + LogLevel, + StepName, + Message + ) + VALUES + ( + @RunId, + 'RUN', + NULL, + 'INFO', + 'INDEX_SELECT_' + @PhaseName, + N'Отбор завершен. Добавлено кандидатов: ' + CAST(@Inserted AS nvarchar(20)) + ); +END; +GO + +CREATE OR ALTER PROCEDURE dbo.usp_UpdateStatistics_SelectCandidates +( + @DatabaseName sysname, + @RunId bigint +) +AS +BEGIN + SET NOCOUNT ON; + SET XACT_ABORT ON; + + DECLARE + @IsEnabled bit, + @MinRows bigint, + @MinModPct decimal(5,2), + @MinModAbs bigint, + @Cmd nvarchar(max), + @Inserted int = 0, + @AdminDb sysname = DB_NAME(); + + IF @DatabaseName IS NULL OR LTRIM(RTRIM(@DatabaseName)) = N'' + THROW 50031, N'usp_UpdateStatistics_SelectCandidates: не задан параметр @DatabaseName.', 1; + + IF @RunId IS NULL + THROW 50032, N'usp_UpdateStatistics_SelectCandidates: не задан параметр @RunId.', 1; + + IF DB_ID(@DatabaseName) IS NULL + THROW 50033, N'usp_UpdateStatistics_SelectCandidates: указанная база не существует.', 1; + + SELECT + @IsEnabled = IsEnabled, + @MinRows = MinRows, + @MinModPct = MinModPct, + @MinModAbs = MinModAbs + FROM dbo.MaintenanceConfig + WHERE DatabaseName = @DatabaseName; + + IF @IsEnabled IS NULL + THROW 50034, N'usp_UpdateStatistics_SelectCandidates: для базы отсутствует запись в dbo.MaintenanceConfig.', 1; + + IF @IsEnabled = 0 + BEGIN + INSERT INTO dbo.MaintenanceLog + ( + RunId, + TaskType, + TaskId, + LogLevel, + StepName, + Message + ) + VALUES + ( + @RunId, + 'RUN', + NULL, + 'WARN', + 'STATS_SELECT', + N'Этап пропущен: обслуживание для базы отключено в dbo.MaintenanceConfig.' + ); + + RETURN; + END; + + INSERT INTO dbo.MaintenanceLog + ( + RunId, + TaskType, + TaskId, + LogLevel, + StepName, + Message + ) + VALUES + ( + @RunId, + 'RUN', + NULL, + 'INFO', + 'STATS_SELECT', + N'Начат отбор кандидатов на обновление статистики для базы ' + QUOTENAME(@DatabaseName) + ); + + SET @Cmd = N' + USE ' + QUOTENAME(@DatabaseName) + N'; + + ;WITH StatsData AS + ( + SELECT + o.object_id, + s.name AS SchemaName, + o.name AS TableName, + CASE + WHEN o.name LIKE ''[_]AccRg%'' + OR o.name LIKE ''[_]AccumRg%'' + OR o.name LIKE ''[_]InfoRg%'' + OR o.name LIKE ''[_]CRg%'' + THEN 1 + ELSE 0 + END AS Is1CRegister, + ISNULL(sp.[rows], 0) AS [rows], + ISNULL(sp.modification_counter, 0) AS modification_counter, + CASE + WHEN ISNULL(sp.[rows], 0) = 0 THEN 0 + ELSE + CASE + WHEN (100.0 * ISNULL(sp.modification_counter, 0) / sp.[rows]) > 99999 + THEN 99999 + ELSE (100.0 * ISNULL(sp.modification_counter, 0) / sp.[rows]) + END + END AS ModPct, + sp.last_updated + FROM sys.objects AS o + JOIN sys.schemas AS s + ON s.schema_id = o.schema_id + JOIN sys.stats AS st + ON st.object_id = o.object_id + OUTER APPLY sys.dm_db_stats_properties(st.object_id, st.stats_id) AS sp + WHERE + o.type = ''U'' + AND o.is_ms_shipped = 0 + ), + Agg AS + ( + SELECT + sd.object_id, + sd.SchemaName, + sd.TableName, + MAX(sd.Is1CRegister) AS Is1CRegister, + MAX(sd.[rows]) AS RowsCount, + MAX(sd.modification_counter) AS ModificationCounter, + MAX(sd.ModPct) AS ModPct, + MAX(CASE WHEN sd.last_updated IS NULL THEN 1 ELSE 0 END) AS HasNeverUpdatedStats, + SUM(CASE WHEN sd.modification_counter >= @p_MinModAbs THEN 1 ELSE 0 END) AS StatsWithHighModCounter + FROM StatsData AS sd + WHERE sd.[rows] >= @p_MinRows + GROUP BY + sd.object_id, + sd.SchemaName, + sd.TableName + ), + Prepared AS + ( + SELECT + a.object_id, + a.SchemaName, + a.TableName, + a.Is1CRegister, + a.RowsCount, + a.ModificationCounter, + a.ModPct, + CASE + WHEN a.Is1CRegister = 1 THEN ''FULLSCAN'' + WHEN a.RowsCount < 5000000 THEN ''FULLSCAN'' + ELSE ''SAMPLE'' + END AS UpdateMode, + CASE + WHEN a.Is1CRegister = 1 THEN NULL + WHEN a.RowsCount < 5000000 THEN NULL + WHEN a.RowsCount < 10000000 THEN 30 + WHEN a.RowsCount < 30000000 THEN 20 + WHEN a.RowsCount < 100000000 THEN 10 + WHEN a.RowsCount < 300000000 THEN 7 + WHEN a.RowsCount < 1000000000 THEN 5 + WHEN a.RowsCount < 3000000000 THEN 4 + ELSE 3 + END AS SamplePercent + FROM Agg AS a + WHERE + a.HasNeverUpdatedStats = 1 + OR a.ModPct >= @p_MinModPct + OR a.StatsWithHighModCounter >= 1 + ) + INSERT INTO ' + QUOTENAME(@AdminDb) + N'.dbo.StatsMaintenanceTask + ( + RunId, + DatabaseName, + SchemaName, + TableName, + ObjectId, + StatsName, + RowsCount, + ModificationCounter, + ModPct, + UpdateMode, + SamplePercent, + Status, + CommandText + ) + SELECT + @p_RunId, + @p_DatabaseName, + p.SchemaName, + p.TableName, + p.object_id, + NULL, + p.RowsCount, + p.ModificationCounter, + p.ModPct, + p.UpdateMode, + p.SamplePercent, + ''PENDING'', + NULL + FROM Prepared AS p + WHERE NOT EXISTS + ( + SELECT 1 + FROM ' + QUOTENAME(@AdminDb) + N'.dbo.StatsMaintenanceTask AS t + WHERE t.RunId = @p_RunId + AND t.DatabaseName = @p_DatabaseName + AND t.ObjectId = p.object_id + ); + + SET @p_Inserted = @@ROWCOUNT; + '; + + EXEC sys.sp_executesql + @Cmd, + N'@p_RunId bigint, + @p_DatabaseName sysname, + @p_MinRows bigint, + @p_MinModPct decimal(5,2), + @p_MinModAbs bigint, + @p_Inserted int OUTPUT', + @p_RunId = @RunId, + @p_DatabaseName = @DatabaseName, + @p_MinRows = @MinRows, + @p_MinModPct = @MinModPct, + @p_MinModAbs = @MinModAbs, + @p_Inserted = @Inserted OUTPUT; + + INSERT INTO dbo.MaintenanceLog + ( + RunId, TaskType, TaskId, LogLevel, StepName, Message + ) + VALUES + ( + @RunId, 'RUN', NULL, 'INFO', 'STATS_SELECT', + N'Отбор кандидатов на обновление статистики завершен. Добавлено задач: ' + CAST(@Inserted AS nvarchar(20)) + N'.' + ); +END; +GO + +CREATE OR ALTER PROCEDURE dbo.usp_Index_RebuildProcess +( + @DatabaseName sysname, + @RunId bigint, + @PhaseName varchar(20) +) +AS +BEGIN + SET NOCOUNT ON; + SET XACT_ABORT OFF; + + DECLARE + @MaxDop int, + @LockTimeoutMs int, + @RebuildCutoffTime time(0), + @IsEnabled bit, + @TaskId bigint, + @SchemaName sysname, + @TableName sysname, + @IndexName sysname, + @ObjectId int, + @IndexId int, + @PageCount bigint, + @FragPercent decimal(8,3), + @Cmd nvarchar(max), + @StartTime datetime2(0), + @EndTime datetime2(0), + @DurationMin decimal(10,2), + @CutoffDateTime datetime2(0), + @SkippedByCutoff int = 0; + + IF @DatabaseName IS NULL OR LTRIM(RTRIM(@DatabaseName)) = N'' + THROW 50011, N'usp_Index_RebuildProcess: не задан параметр @DatabaseName.', 1; + + IF @RunId IS NULL + THROW 50012, N'usp_Index_RebuildProcess: не задан параметр @RunId.', 1; + + IF DB_ID(@DatabaseName) IS NULL + THROW 50013, N'usp_Index_RebuildProcess: указанная база не существует.', 1; + + IF @PhaseName IS NULL OR UPPER(@PhaseName) NOT IN ('LIMITED', 'SAMPLED') + THROW 50014, N'usp_Index_RebuildProcess: @PhaseName должен быть LIMITED или SAMPLED.', 1; + + SET @PhaseName = UPPER(@PhaseName); + + SELECT + @IsEnabled = IsEnabled, + @MaxDop = MaxDop, + @LockTimeoutMs = LockTimeoutMs, + @RebuildCutoffTime = RebuildCutoffTime + FROM dbo.MaintenanceConfig + WHERE DatabaseName = @DatabaseName; + + IF @IsEnabled IS NULL + THROW 50015, N'usp_Index_RebuildProcess: для базы отсутствует запись в dbo.MaintenanceConfig.', 1; + + IF @IsEnabled = 0 + BEGIN + INSERT INTO dbo.MaintenanceLog + ( + RunId, TaskType, TaskId, LogLevel, StepName, Message + ) + VALUES + ( + @RunId, 'RUN', NULL, 'WARN', + 'INDEX_REBUILD_' + @PhaseName, + N'Этап пропущен: обслуживание для базы отключено в dbo.MaintenanceConfig.' + ); + + RETURN; + END; + + IF @RebuildCutoffTime IS NOT NULL + BEGIN + SET @CutoffDateTime = + DATEADD + ( + SECOND, + DATEDIFF(SECOND, CONVERT(time(0), '00:00:00'), @RebuildCutoffTime), + CONVERT(datetime2(0), CONVERT(date, SYSDATETIME())) + ); + END; + + DECLARE @SqlLockTimeout nvarchar(100); + SET @SqlLockTimeout = N'SET LOCK_TIMEOUT ' + CONVERT(nvarchar(20), @LockTimeoutMs) + N';'; + EXEC sys.sp_executesql @SqlLockTimeout; + + INSERT INTO dbo.MaintenanceLog + ( + RunId, + TaskType, + TaskId, + LogLevel, + StepName, + Message + ) + VALUES + ( + @RunId, + 'RUN', + NULL, + 'INFO', + 'INDEX_REBUILD_' + @PhaseName, + N'Начата обработка очереди индексов для фазы ' + @PhaseName + N' в базе ' + QUOTENAME(@DatabaseName) + ); + + WHILE EXISTS + ( + SELECT 1 + FROM dbo.IndexMaintenanceTask + WHERE RunId = @RunId + AND DatabaseName = @DatabaseName + AND ScanMode = @PhaseName + AND Status = 'PENDING' + ) + BEGIN + IF @CutoffDateTime IS NOT NULL AND SYSDATETIME() >= @CutoffDateTime + BEGIN + UPDATE dbo.IndexMaintenanceTask + SET + Status = 'TIME_LIMIT', + SkipReason = 'TIME_LIMIT', + EndTime = SYSDATETIME(), + ErrorMessage = N'Операция не начата: достигнуто ограничение RebuildCutoffTime.' + WHERE RunId = @RunId + AND DatabaseName = @DatabaseName + AND ScanMode = @PhaseName + AND Status = 'PENDING'; + + SET @SkippedByCutoff = @@ROWCOUNT; + + INSERT INTO dbo.MaintenanceLog + ( + RunId, + TaskType, + TaskId, + LogLevel, + StepName, + Message + ) + VALUES + ( + @RunId, + 'RUN', + NULL, + 'WARN', + 'INDEX_REBUILD_' + @PhaseName, + N'Обработка очереди остановлена по RebuildCutoffTime. Пропущено задач: ' + CAST(@SkippedByCutoff AS nvarchar(20)) + ); + + BREAK; + END; + + SELECT TOP (1) + @TaskId = TaskId, + @SchemaName = SchemaName, + @TableName = TableName, + @IndexName = IndexName, + @ObjectId = ObjectId, + @IndexId = IndexId, + @PageCount = PageCount, + @FragPercent = FragPercent + FROM dbo.IndexMaintenanceTask + WHERE RunId = @RunId + AND DatabaseName = @DatabaseName + AND ScanMode = @PhaseName + AND Status = 'PENDING' + ORDER BY FragPercent DESC, PageCount DESC, TaskId ASC; + + SET @StartTime = SYSDATETIME(); + + UPDATE dbo.IndexMaintenanceTask + SET + Status = 'RUNNING', + StartTime = @StartTime, + CommandText = + N'USE ' + QUOTENAME(@DatabaseName) + N'; ' + + N'ALTER INDEX ' + QUOTENAME(@IndexName) + + N' ON ' + QUOTENAME(@SchemaName) + N'.' + QUOTENAME(@TableName) + + N' REBUILD WITH (SORT_IN_TEMPDB = OFF, MAXDOP = ' + CAST(@MaxDop AS nvarchar(10)) + N', ONLINE = OFF);' + WHERE TaskId = @TaskId; + + SELECT @Cmd = CommandText + FROM dbo.IndexMaintenanceTask + WHERE TaskId = @TaskId; + + BEGIN TRY + EXEC sys.sp_executesql @Cmd; + + SET @EndTime = SYSDATETIME(); + SET @DurationMin = CAST(DATEDIFF(MILLISECOND, @StartTime, @EndTime) / 60000.0 AS decimal(10,2)); + + UPDATE dbo.IndexMaintenanceTask + SET + Status = 'DONE', + SkipReason = NULL, + EndTime = @EndTime, + DurationMin = @DurationMin, + ErrorNumber = NULL, + ErrorMessage = NULL + WHERE TaskId = @TaskId; + END TRY + BEGIN CATCH + SET @EndTime = SYSDATETIME(); + SET @DurationMin = CAST(DATEDIFF(MILLISECOND, @StartTime, @EndTime) / 60000.0 AS decimal(10,2)); + + IF ERROR_NUMBER() = 1222 + BEGIN + UPDATE dbo.IndexMaintenanceTask + SET + Status = 'LOCK_TIMEOUT', + SkipReason = 'LOCK_TIMEOUT', + EndTime = @EndTime, + DurationMin = @DurationMin, + ErrorNumber = ERROR_NUMBER(), + ErrorMessage = ERROR_MESSAGE() + WHERE TaskId = @TaskId; + + INSERT INTO dbo.MaintenanceLog + ( + RunId, + TaskType, + TaskId, + LogLevel, + StepName, + Message + ) + VALUES + ( + @RunId, + 'INDEX', + @TaskId, + 'WARN', + 'INDEX_REBUILD_' + @PhaseName, + N'LOCK_TIMEOUT при REBUILD индекса ' + QUOTENAME(@SchemaName) + N'.' + QUOTENAME(@TableName) + N' / ' + QUOTENAME(@IndexName) + ); + END + ELSE IF ERROR_NUMBER() = 1205 + BEGIN + UPDATE dbo.IndexMaintenanceTask + SET + Status = 'DEADLOCK', + SkipReason = 'DEADLOCK', + EndTime = @EndTime, + DurationMin = @DurationMin, + ErrorNumber = ERROR_NUMBER(), + ErrorMessage = ERROR_MESSAGE() + WHERE TaskId = @TaskId; + + INSERT INTO dbo.MaintenanceLog + ( + RunId, + TaskType, + TaskId, + LogLevel, + StepName, + Message + ) + VALUES + ( + @RunId, + 'INDEX', + @TaskId, + 'WARN', + 'INDEX_REBUILD_' + @PhaseName, N'DEADLOCK при REBUILD индекса ' + QUOTENAME(@SchemaName) + N'.' + QUOTENAME(@TableName) + N' / ' + QUOTENAME(@IndexName) + ); + + END + ELSE + BEGIN + UPDATE dbo.IndexMaintenanceTask + SET + Status = 'ERROR', + SkipReason = NULL, + EndTime = @EndTime, + DurationMin = @DurationMin, + ErrorNumber = ERROR_NUMBER(), + ErrorMessage = ERROR_MESSAGE() + WHERE TaskId = @TaskId; + + INSERT INTO dbo.MaintenanceLog + ( + RunId, + TaskType, + TaskId, + LogLevel, + StepName, + Message + ) + VALUES + ( + @RunId, + 'INDEX', + @TaskId, + 'ERROR', + 'INDEX_REBUILD_' + @PhaseName, + N'Ошибка при REBUILD индекса ' + QUOTENAME(@SchemaName) + N'.' + QUOTENAME(@TableName) + N' / ' + QUOTENAME(@IndexName) + N'. Ошибка: ' + ERROR_MESSAGE() + ); + END + END CATCH; + END; + + INSERT INTO dbo.MaintenanceLog + ( + RunId, + TaskType, + TaskId, + LogLevel, + StepName, + Message + ) + VALUES + ( + @RunId, + 'RUN', + NULL, + 'INFO', + 'INDEX_REBUILD_' + @PhaseName, + N'Обработка очереди индексов для фазы ' + @PhaseName + N' завершена.' + ); +END; +GO + +CREATE OR ALTER PROCEDURE dbo.usp_UpdateStatistics_Process +( + @DatabaseName sysname, + @RunId bigint +) +AS +BEGIN + SET NOCOUNT ON; + SET XACT_ABORT OFF; + + DECLARE + @IsEnabled bit, + @TaskId bigint, + @SchemaName sysname, + @TableName sysname, + @StatsName sysname, + @UpdateMode varchar(20), + @SamplePercent int, + @Cmd nvarchar(max), + @StartTime datetime2(0), + @EndTime datetime2(0), + @RebuildCutoffTime time(0), + @CutoffDateTime datetime2(0), + @DurationMin decimal(10,2), + @SkippedByCutoff int = 0; + + IF @DatabaseName IS NULL OR LTRIM(RTRIM(@DatabaseName)) = N'' + THROW 50041, N'usp_UpdateStatistics_Process: не задан параметр @DatabaseName.', 1; + + IF @RunId IS NULL + THROW 50042, N'usp_UpdateStatistics_Process: не задан параметр @RunId.', 1; + + IF DB_ID(@DatabaseName) IS NULL + THROW 50043, N'usp_UpdateStatistics_Process: указанная база не существует.', 1; + + SELECT + @IsEnabled = IsEnabled, + @RebuildCutoffTime = RebuildCutoffTime + FROM dbo.MaintenanceConfig + WHERE DatabaseName = @DatabaseName; + + IF @IsEnabled IS NULL + THROW 50044, N'usp_UpdateStatistics_Process: для базы отсутствует запись в dbo.MaintenanceConfig.', 1; + + IF @IsEnabled = 0 + BEGIN + INSERT INTO dbo.MaintenanceLog + ( + RunId, TaskType, TaskId, LogLevel, StepName, Message + ) + VALUES + ( + @RunId, 'RUN', NULL, 'WARN', 'STATS_UPDATE', + N'Этап пропущен: обслуживание для базы отключено в dbo.MaintenanceConfig.' + ); + + RETURN; + END; + + IF @RebuildCutoffTime IS NOT NULL + BEGIN + SET @CutoffDateTime = + DATEADD + ( + SECOND, + DATEDIFF(SECOND, CONVERT(time(0), '00:00:00'), @RebuildCutoffTime), + CONVERT(datetime2(0), CONVERT(date, SYSDATETIME())) + ); + END; + + INSERT INTO dbo.MaintenanceLog + ( + RunId, + TaskType, + TaskId, + LogLevel, + StepName, + Message + ) + VALUES + ( + @RunId, + 'RUN', + NULL, + 'INFO', + 'STATS_UPDATE', + N'Начато обновление статистики для базы ' + QUOTENAME(@DatabaseName) + ); + + WHILE EXISTS + ( + SELECT 1 + FROM dbo.StatsMaintenanceTask + WHERE RunId = @RunId + AND DatabaseName = @DatabaseName + AND Status = 'PENDING' + ) + BEGIN + IF @CutoffDateTime IS NOT NULL AND SYSDATETIME() >= @CutoffDateTime + BEGIN + UPDATE dbo.StatsMaintenanceTask + SET + Status = 'TIME_LIMIT', + EndTime = SYSDATETIME(), + ErrorMessage = N'Операция не начата: достигнуто ограничение RebuildCutoffTime.' + WHERE RunId = @RunId + AND DatabaseName = @DatabaseName + AND Status = 'PENDING'; + + SET @SkippedByCutoff = @@ROWCOUNT; + + INSERT INTO dbo.MaintenanceLog + ( + RunId, + TaskType, + TaskId, + LogLevel, + StepName, + Message + ) + VALUES + ( + @RunId, + 'RUN', + NULL, + 'WARN', + 'STATS_UPDATE', + N'Обработка очереди остановлена по RebuildCutoffTime. Пропущено задач: ' + CAST(@SkippedByCutoff AS nvarchar(20)) + ); + + BREAK; + END; + + SELECT TOP (1) + @TaskId = TaskId, + @SchemaName = SchemaName, + @TableName = TableName, + @StatsName = StatsName, + @UpdateMode = UpdateMode, + @SamplePercent = SamplePercent + FROM dbo.StatsMaintenanceTask + WHERE RunId = @RunId + AND DatabaseName = @DatabaseName + AND Status = 'PENDING' + ORDER BY TaskId; + + SET @StartTime = SYSDATETIME(); + + UPDATE dbo.StatsMaintenanceTask + SET Status = 'RUNNING', + StartTime = @StartTime + WHERE TaskId = @TaskId; + + SET @Cmd = + N'USE ' + QUOTENAME(@DatabaseName) + N'; ' + + N'UPDATE STATISTICS ' + QUOTENAME(@SchemaName) + N'.' + QUOTENAME(@TableName) + + CASE + WHEN @StatsName IS NOT NULL + THEN N' (' + QUOTENAME(@StatsName) + N')' + ELSE N'' + END + + CASE + WHEN @UpdateMode = 'FULLSCAN' + THEN N' WITH FULLSCAN;' + WHEN @UpdateMode = 'SAMPLE' + THEN N' WITH SAMPLE ' + CAST(@SamplePercent AS nvarchar(10)) + N' PERCENT;' + ELSE N';' + END; + + UPDATE dbo.StatsMaintenanceTask + SET CommandText = @Cmd + WHERE TaskId = @TaskId; + + BEGIN TRY + EXEC sys.sp_executesql @Cmd; + + SET @EndTime = SYSDATETIME(); + SET @DurationMin = CAST(DATEDIFF(MILLISECOND, @StartTime, @EndTime) / 60000.0 AS decimal(10,2)); + + UPDATE dbo.StatsMaintenanceTask + SET + Status = 'DONE', + EndTime = @EndTime, + DurationMin = @DurationMin, + ErrorNumber = NULL, + ErrorMessage = NULL + WHERE TaskId = @TaskId; + END TRY + BEGIN CATCH + SET @EndTime = SYSDATETIME(); + SET @DurationMin = CAST(DATEDIFF(MILLISECOND, @StartTime, @EndTime) / 60000.0 AS decimal(10,2)); + + UPDATE dbo.StatsMaintenanceTask + SET + Status = 'ERROR', + EndTime = @EndTime, + DurationMin = @DurationMin, + ErrorNumber = ERROR_NUMBER(), + ErrorMessage = ERROR_MESSAGE() + WHERE TaskId = @TaskId; + + INSERT INTO dbo.MaintenanceLog + ( + RunId, + TaskType, + TaskId, + LogLevel, + StepName, + Message + ) + VALUES + ( + @RunId, + 'STATS', + @TaskId, + 'ERROR', + 'STATS_UPDATE', + N'Ошибка обновления статистики для ' + QUOTENAME(@SchemaName) + N'.' + QUOTENAME(@TableName) + N'. Ошибка: ' + ERROR_MESSAGE() + ); + END CATCH; + END; + + INSERT INTO dbo.MaintenanceLog + ( + RunId, + TaskType, + TaskId, + LogLevel, + StepName, + Message + ) + VALUES + ( + @RunId, + 'RUN', + NULL, + 'INFO', + 'STATS_UPDATE', + N'Обновление статистики для базы ' + QUOTENAME(@DatabaseName) + N' завершено' + ); +END; +GO + +CREATE OR ALTER PROCEDURE dbo.usp_RunMaintenance +( + @DatabaseName sysname, + @RunId bigint OUTPUT +) +AS +BEGIN + SET NOCOUNT ON; + SET XACT_ABORT ON; + + DECLARE + @IsEnabled bit, + @MinPages int, + @MinFrag decimal(5,2), + @MaxDop int, + @MinRows bigint, + @MinModPct decimal(5,2), + @MinModAbs bigint, + @LockTimeoutMs int, + @RebuildCutoffTime time(0), + @CutoffDateTime datetime2(0), + @ErrorMessage nvarchar(max), + @FinalStatus varchar(20), + @IndexCandidates int, + @IndexProcessed int, + @IndexErrors int, + @IndexLockTimeouts int, + @IndexDeadlocks int, + @IndexTimeLimitSkipped int, + @StatsCandidates int, + @StatsProcessed int, + @StatsErrors int; + + IF @DatabaseName IS NULL OR LTRIM(RTRIM(@DatabaseName)) = N'' + THROW 50101, N'usp_RunMaintenance: не задан параметр @DatabaseName.', 1; + + IF DB_ID(@DatabaseName) IS NULL + THROW 50102, N'usp_RunMaintenance: указанная база не существует.', 1; + + SELECT + @IsEnabled = IsEnabled, + @MinPages = MinPages, + @MinFrag = MinFrag, + @MaxDop = MaxDop, + @MinRows = MinRows, + @MinModPct = MinModPct, + @MinModAbs = MinModAbs, + @LockTimeoutMs = LockTimeoutMs, + @RebuildCutoffTime = RebuildCutoffTime + FROM dbo.MaintenanceConfig + WHERE DatabaseName = @DatabaseName; + + IF @IsEnabled IS NULL + THROW 50103, N'usp_RunMaintenance: для базы отсутствует запись в dbo.MaintenanceConfig.', 1; + + IF @RebuildCutoffTime IS NOT NULL + BEGIN + SET @CutoffDateTime = + DATEADD + ( + SECOND, + DATEDIFF(SECOND, CONVERT(time(0), '00:00:00'), @RebuildCutoffTime), + CONVERT(datetime2(0), CONVERT(date, SYSDATETIME())) + ); + END; + + INSERT INTO dbo.MaintenanceRun + ( + ServerName, + DatabaseName, + JobName, + Initiator, + SessionId, + StartTime, + Status, + MinPages, + MinFrag, + MaxDop, + MinRows, + MinModPct, + MinModAbs, + LockTimeoutMs, + CutoffTime + ) + VALUES + ( + @@SERVERNAME, + @DatabaseName, + NULL, + 'SQLAGENT', + @@SPID, + SYSDATETIME(), + 'RUNNING', + @MinPages, + @MinFrag, + @MaxDop, + @MinRows, + @MinModPct, + @MinModAbs, + @LockTimeoutMs, + @CutoffDateTime + ); + + SET @RunId = SCOPE_IDENTITY(); + + INSERT INTO dbo.MaintenanceLog + ( + RunId, + TaskType, + TaskId, + LogLevel, + StepName, + Message + ) + VALUES + ( + @RunId, + 'RUN', + NULL, + 'INFO', + 'START_RUN', + N'Старт обслуживания базы ' + QUOTENAME(@DatabaseName) + ); + + IF @IsEnabled = 0 + BEGIN + INSERT INTO dbo.MaintenanceLog + ( + RunId, TaskType, TaskId, LogLevel, StepName, Message + ) + VALUES + ( + @RunId, 'RUN', NULL, 'WARN', 'CANCELLED_RUN', + N'Обслуживание базы отключено в dbo.MaintenanceConfig. Отмена' + ); + + UPDATE dbo.MaintenanceRun + SET + EndTime = SYSDATETIME(), + Status = 'CANCELLED', + ErrorMessage = NULL, + IndexCandidates = 0, + IndexProcessed = 0, + IndexErrors = 0, + IndexLockTimeouts = 0, + IndexDeadlocks = 0, + IndexTimeLimitSkipped = 0, + StatsCandidates = 0, + StatsProcessed = 0, + StatsErrors = 0 + WHERE RunId = @RunId; + + RETURN; + END; + + BEGIN TRY + EXEC dbo.usp_Index_SelectCandidates + @DatabaseName = @DatabaseName, + @RunId = @RunId, + @PhaseName = 'LIMITED'; + + EXEC dbo.usp_Index_RebuildProcess + @DatabaseName = @DatabaseName, + @RunId = @RunId, + @PhaseName = 'LIMITED'; + + EXEC dbo.usp_Index_SelectCandidates + @DatabaseName = @DatabaseName, + @RunId = @RunId, + @PhaseName = 'SAMPLED'; + + EXEC dbo.usp_Index_RebuildProcess + @DatabaseName = @DatabaseName, + @RunId = @RunId, + @PhaseName = 'SAMPLED'; + + EXEC dbo.usp_UpdateStatistics_SelectCandidates + @DatabaseName = @DatabaseName, + @RunId = @RunId; + + EXEC dbo.usp_UpdateStatistics_Process + @DatabaseName = @DatabaseName, + @RunId = @RunId; + + SELECT + @IndexCandidates = COUNT(*), + @IndexProcessed = SUM(CASE WHEN Status = 'DONE' THEN 1 ELSE 0 END), + @IndexErrors = SUM(CASE WHEN Status = 'ERROR' THEN 1 ELSE 0 END), + @IndexLockTimeouts = SUM(CASE WHEN Status = 'LOCK_TIMEOUT' THEN 1 ELSE 0 END), + @IndexDeadlocks = SUM(CASE WHEN Status = 'DEADLOCK' THEN 1 ELSE 0 END), + @IndexTimeLimitSkipped = SUM(CASE WHEN Status = 'TIME_LIMIT' THEN 1 ELSE 0 END) + FROM dbo.IndexMaintenanceTask + WHERE RunId = @RunId + AND DatabaseName = @DatabaseName; + + SELECT + @StatsCandidates = COUNT(*), + @StatsProcessed = SUM(CASE WHEN Status = 'DONE' THEN 1 ELSE 0 END), + @StatsErrors = SUM(CASE WHEN Status = 'ERROR' THEN 1 ELSE 0 END) + FROM dbo.StatsMaintenanceTask + WHERE RunId = @RunId + AND DatabaseName = @DatabaseName; + + SELECT + @IndexCandidates = ISNULL(@IndexCandidates, 0), + @IndexProcessed = ISNULL(@IndexProcessed, 0), + @IndexErrors = ISNULL(@IndexErrors, 0), + @IndexLockTimeouts = ISNULL(@IndexLockTimeouts, 0), + @IndexDeadlocks = ISNULL(@IndexDeadlocks, 0), + @IndexTimeLimitSkipped = ISNULL(@IndexTimeLimitSkipped, 0), + @StatsCandidates = ISNULL(@StatsCandidates, 0), + @StatsProcessed = ISNULL(@StatsProcessed, 0), + @StatsErrors = ISNULL(@StatsErrors, 0); + + IF @IndexErrors > 0 OR @StatsErrors > 0 + SET @FinalStatus = 'ERROR'; + ELSE IF @IndexLockTimeouts > 0 OR @IndexDeadlocks > 0 OR @IndexTimeLimitSkipped > 0 + SET @FinalStatus = 'WARNING'; + ELSE + SET @FinalStatus = 'SUCCESS'; + + UPDATE dbo.MaintenanceRun + SET + EndTime = SYSDATETIME(), + Status = @FinalStatus, + ErrorMessage = NULL, + IndexCandidates = @IndexCandidates, + IndexProcessed = @IndexProcessed, + IndexErrors = @IndexErrors, + IndexLockTimeouts = @IndexLockTimeouts, + IndexDeadlocks = @IndexDeadlocks, + IndexTimeLimitSkipped = @IndexTimeLimitSkipped, + StatsCandidates = @StatsCandidates, + StatsProcessed = @StatsProcessed, + StatsErrors = @StatsErrors + WHERE RunId = @RunId; + + INSERT INTO dbo.MaintenanceLog + ( + RunId, TaskType, TaskId, LogLevel, StepName, Message + ) + VALUES + ( + @RunId, 'RUN', NULL, 'INFO', 'FINISH_RUN', + N'Запуск обслуживания базы ' + QUOTENAME(@DatabaseName) + + N' завершен. Итоговый статус: ' + @FinalStatus + N'.' + ); + END TRY + BEGIN CATCH + SET @ErrorMessage = ERROR_MESSAGE(); + + SELECT + @IndexCandidates = COUNT(*), + @IndexProcessed = SUM(CASE WHEN Status = 'DONE' THEN 1 ELSE 0 END), + @IndexErrors = SUM(CASE WHEN Status = 'ERROR' THEN 1 ELSE 0 END), + @IndexLockTimeouts = SUM(CASE WHEN Status = 'LOCK_TIMEOUT' THEN 1 ELSE 0 END), + @IndexDeadlocks = SUM(CASE WHEN Status = 'DEADLOCK' THEN 1 ELSE 0 END), + @IndexTimeLimitSkipped = SUM(CASE WHEN Status = 'TIME_LIMIT' THEN 1 ELSE 0 END) + FROM dbo.IndexMaintenanceTask + WHERE RunId = @RunId + AND DatabaseName = @DatabaseName; + + SELECT + @StatsCandidates = COUNT(*), + @StatsProcessed = SUM(CASE WHEN Status = 'DONE' THEN 1 ELSE 0 END), + @StatsErrors = SUM(CASE WHEN Status = 'ERROR' THEN 1 ELSE 0 END) + FROM dbo.StatsMaintenanceTask + WHERE RunId = @RunId + AND DatabaseName = @DatabaseName; + + SELECT + @IndexCandidates = ISNULL(@IndexCandidates, 0), + @IndexProcessed = ISNULL(@IndexProcessed, 0), + @IndexErrors = ISNULL(@IndexErrors, 0), + @IndexLockTimeouts = ISNULL(@IndexLockTimeouts, 0), + @IndexDeadlocks = ISNULL(@IndexDeadlocks, 0), + @IndexTimeLimitSkipped = ISNULL(@IndexTimeLimitSkipped, 0), + @StatsCandidates = ISNULL(@StatsCandidates, 0), + @StatsProcessed = ISNULL(@StatsProcessed, 0), + @StatsErrors = ISNULL(@StatsErrors, 0); + + UPDATE dbo.MaintenanceRun + SET + EndTime = SYSDATETIME(), + Status = 'ERROR', + ErrorMessage = @ErrorMessage, + IndexCandidates = @IndexCandidates, + IndexProcessed = @IndexProcessed, + IndexErrors = @IndexErrors, + IndexLockTimeouts = @IndexLockTimeouts, + IndexDeadlocks = @IndexDeadlocks, + IndexTimeLimitSkipped = @IndexTimeLimitSkipped, + StatsCandidates = @StatsCandidates, + StatsProcessed = @StatsProcessed, + StatsErrors = @StatsErrors + WHERE RunId = @RunId; + + INSERT INTO dbo.MaintenanceLog + ( + RunId, TaskType, TaskId, LogLevel, StepName, Message + ) + VALUES + ( + @RunId, 'RUN', NULL, 'ERROR', 'RUN_ERROR', + N'Запуск обслуживания базы ' + QUOTENAME(@DatabaseName) + + N' завершился с ошибкой: ' + @ErrorMessage + ); + + THROW; + END CATCH; +END; +GO + +/*----------------------------------------------------------- + Отображаем конфиг +------------------------------------------------------------*/ + +SELECT * +FROM dbo.MaintenanceConfig +ORDER BY DatabaseName; +GO \ No newline at end of file