Стратегия обслуживания баз — VLDB Переиндексация таблиц

от автора

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

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

Переиндексация

Основная проблема при обслуживании больших баз данных (VLDB) заключается в том, что переиндексация всех таблиц может занимать очень продолжительное время, иногда растягиваясь на несколько дней. Это создает серьезные трудности для обеспечения непрерывности бизнес-процессов и требует специальных подходов к организации данного процесса.

Проблемы стандартной переиндексации

При работе с крупными базами данных стандартные подходы к переиндексации сталкиваются со следующими проблемами:

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

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

  • Блокировки: многие операции переиндексации требуют установки блокировок, что может приводить к простоям в работе пользователей.

Оптимизированный подход к переиндексации

Для решения указанных проблем предлагается следующий оптимизированный подход:

Вместо переиндексации всех таблиц необходимо разделить таблицы на 2 группы:

  • Горячие таблицы:

    • Высокая частота обращений (запросов) к таблице.

    • Частые изменения данных (INSERT / UPDATE / DELETE).

    • Участие в наиболее важных бизнес-процессах или транзакциях.

    • Критичность для производительности системы.

  • Теплые таблицы:

    • Используются умеренно.

    • Могут быть частью промежуточных процессов или отчетности.

    • Не так критичны для ежедневной операционной нагрузки.

Определение категорий таблиц

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

  • Анализ статистики использования индексов

  • Использование Extended Events

  • Анализ кэша планов запросов

  • Применение SQL Default Trace

  • Анализ трейсов с помощью SQL Profiler

В нашем случае мы выбрали метод анализа статистики использования индексов, поскольку он менее трудоёмкий и минимально влияет на производительность системы. Важно отметить, что любой сбор аналитических данных создаёт дополнительную нагрузку на сервер. Ключевой вопрос состоит в объёме и характере этой нагрузки, а также её влиянии на общую производительность системы.

SELECT      OBJECT_NAME(ius.object_id) AS TableName,     i.name AS IndexName,     i.type_desc AS IndexType,     ius.user_seeks + ius.user_scans + ius.user_lookups AS TotalUserReads,     ius.user_updates AS TotalUserWrites,     ius.last_user_seek,     ius.last_user_scan,     ius.last_user_lookup FROM      sys.dm_db_index_usage_stats ius INNER JOIN      sys.indexes i ON ius.object_id = i.object_id AND ius.index_id = i.index_id WHERE      OBJECTPROPERTY(ius.object_id, 'IsMsShipped') = 0 -- исключить системные таблицы     AND ius.database_id = DB_ID() -- проверим только ткущую БД ORDER BY      TotalUserReads DESC;

Маркировка таблиц

После определения горячих таблиц, необходимо отметить их для дальнейшей обработки. В нашем случае решено добавить расширенное свойство «IsHotTable»

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

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

IF NOT EXISTS (SELECT 1 FROM sys.schemas WHERE name = 'DBA') BEGIN     EXEC('CREATE SCHEMA DBA');     PRINT 'Schema [DBA] created successfully.'; END ELSE BEGIN     PRINT 'Schema [DBA] already exists.'; END
CREATE PROCEDURE dba.SetTableExtendedProperty     @TableName SYSNAME,     @SchemaName SYSNAME = 'dbo',     @PropertyName SYSNAME,     @PropertyValue NVARCHAR(4000) AS BEGIN     SET NOCOUNT ON;      DECLARE @SQL NVARCHAR(MAX);      -- Проверить наличие параметра     IF EXISTS (         SELECT 1         FROM fn_listextendedproperty(@PropertyName, 'SCHEMA', @SchemaName, 'TABLE', @TableName, NULL, NULL)     )     BEGIN         -- Если имеется,  тогда нужно удалить и пересоздать          SET @SQL = '         EXEC sp_dropextendedproperty              @name = N''' + @PropertyName + ''',              @level0type = N''SCHEMA'',              @level0name = N''' + @SchemaName + ''',              @level1type = N''TABLE'',              @level1name = N''' + @TableName + ''';';          EXEC sp_executesql @SQL;     END      -- создать новый     SET @SQL = '     EXEC sp_addextendedproperty          @name = N''' + @PropertyName + ''',          @value = N''' + REPLACE(@PropertyValue, '''', '''''') + ''',          @level0type = N''SCHEMA'',          @level0name = N''' + @SchemaName + ''',          @level1type = N''TABLE'',          @level1name = N''' + @TableName + ''';';      EXEC sp_executesql @SQL;      PRINT 'Extended property [' + @PropertyName + '] updated for [' + @SchemaName + '].[' + @TableName + ']'; END;

☝🏼Важно помнить, что «горячие таблицы» определяются не только по частоте обращений, но и по их важности в бизнес-процессах. Рекомендуется составить список таких таблиц вручную, после чего произвести их маркировку в базе данных.

Пример маркировки

EXEC dba.SetTableExtendedProperty     @TableName = 'Customers',     @SchemaName = 'dbo',     @PropertyName = 'IsHotTable',     @PropertyValue = '1';

Пример: Как получить список с маркировкой IsHotTable

SELECT t.name   AS TableName,        ep.name  AS PropertyName,        ep.value AS PropertyValue FROM sys.tables t          CROSS APPLY      fn_listextendedproperty(default, 'SCHEMA', SCHEMA_NAME(t.schema_id), 'TABLE', t.name, NULL, NULL) ep WHERE t.is_ms_shipped = 0   and ep.name = 'IsHotTable'   and ep.value = '1';

Обслуживание горячих таблиц

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

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

Описание системы многопоточного подхода

Система реализована с использованием стандартного механизма СУБД — SQL Agent Job, в рамках которого создается главное задание (Master Job), которое отвечает за создание и управление подзаданиями (потоками).

Master Job состоит из 2 шагов:

  • Шаг №1 — Создает динамические подзадания в количестве, равном числу потоков.

  • Шаг №2 — отслеживает созданные подзадания и удаляет их после выполнения

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

Создание подсистемы логирования

Для начала необходимо создать таблицу для хранения логов.

SET ANSI_NULLS ON GO  SET QUOTED_IDENTIFIER ON GO  CREATE TABLE [DBA].[ReindexJobLog]( [LogID] [int] IDENTITY(1,1) NOT NULL, [JobName] [nvarchar](128) NULL, [GroupNumber] [int] NULL, [Tablename] [sysname] NOT NULL, [IndexName] [nvarchar](128) NULL, [ActionType] [nvarchar](128) NULL, [StartTime] [datetime] NULL, [EndTime] [datetime] NULL, [DurationSeconds]  AS (datediff(second,[StartTime],[EndTime])), [DBName] [nvarchar](128) NULL, PRIMARY KEY CLUSTERED  ( [LogID] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, FILLFACTOR = 90) ON [FIVE] ) ON [FIVE] GO  ALTER TABLE [DBA].[ReindexJobLog] ADD  DEFAULT (getdate()) FOR [StartTime] GO   

Для более эффективной организации кода и упрощения структуры задания (SQL Job), создадим отдельную хранимую процедуру для процесса переиндексации.

🚨Не забудьте изменить название базы данных в скрипте

SET ANSI_NULLS ON GO SET QUOTED_IDENTIFIER ON GO  Create PROCEDURE [DBA].[ReindexTableWithLogging] @JobName NVARCHAR(128),                                                 @GroupNumber INT,                         @DBName Nvarchar(128),                                                 @TableName SYSNAME,                                                 @SchemaName SYSNAME = 'dbo' AS BEGIN     SET NOCOUNT ON;     DECLARE @FullTableName NVARCHAR(256) = QUOTENAME(@SchemaName) + '.' + QUOTENAME(@TableName);     DECLARE @LogID INT;     DECLARE @indexName NVARCHAR(256);     Declare @sql_updateStats NVARCHAR(max) DECLARE @Batch1_SQL NVARCHAR(MAX);     DECLARE @Batch2_SQL NVARCHAR(MAX);     DECLARE @Batch3_SQL NVARCHAR(MAX);     DECLARE IndexCursor CURSOR LOCAL FAST_FORWARD FOR         SELECT i.name AS IndexName         FROM sys.indexes i         WHERE i.name IS NOT NULL -- Exclude heaps           and object_id = OBJECT_ID(QUOTENAME(@SchemaName) + '.' + QUOTENAME(@TableName))     OPEN IndexCursor;     FETCH NEXT FROM IndexCursor INTO @IndexName;     WHILE @@FETCH_STATUS = 0         begin             INSERT INTO [DatabaseName].dba.ReindexJobLog (JobName, GroupNumber, DBName, Tablename, Indexname, ActionType, StartTime)             VALUES (@JobName, @GroupNumber, @DBName, @FullTableName, @indexName, 'ReBuild Index All indexes', GETDATE());             SET @LogID = SCOPE_IDENTITY();             PRINT 'Starting reindex for group ' + CAST(@GroupNumber AS NVARCHAR(10)) + '\Table' + @FullTableName +                   ' Index ' + @indexname + ' at ' + CONVERT(NVARCHAR, GETDATE(), 120);             -- Run reindex             EXEC ('DBCC DBREINDEX(''' + @FullTableName + ''', ''' + @indexname + ''', 90);');             -- Update log             UPDATE [DatabaseName].DBA.ReindexJobLog             SET EndTime = GETDATE()             WHERE LogID = @LogID               and Tablename = @FullTableName               and indexname = @indexName;             PRINT 'Finished reindexing group ' + CAST(@GroupNumber AS NVARCHAR(10)) + '\Table' + @FullTableName +                   ' at ' + CONVERT(NVARCHAR, GETDATE(), 120);             FETCH NEXT FROM IndexCursor INTO @IndexName;         END     INSERT INTO [DatabaseName].dba.ReindexJobLog (JobName, GroupNumber, DBName, Tablename, Indexname, ActionType, StartTime)     VALUES (@JobName, @GroupNumber, @DBName,  @FullTableName, '', 'Update rest of statistics', GETDATE());     SET @LogID = SCOPE_IDENTITY();    if         (SELECT count(1)          FROM sys.stats s                   INNER JOIN sys.objects obj ON s.object_id = obj.object_id                   INNER JOIN sys.schemas sch ON obj.schema_id = sch.schema_id          WHERE obj.type = 'U' -- Only user tables            AND sch.name = @SchemaName            AND obj.name = @TableName            AND NOT EXISTS (SELECT 1                            FROM sys.indexes i                            WHERE i.object_id = s.object_id                              AND i.index_id = s.stats_id)) < 20         Begin              SELECT @sql_updateStats = STRING_AGG(                     'UPDATE STATISTICS ' + QUOTENAME(sch.name) + '.' + QUOTENAME(obj.name)                         + ' ' + QUOTENAME(s.name) + ' WITH FULLSCAN;',                     CHAR(13)                                       )             FROM sys.stats s                      INNER JOIN sys.objects obj ON s.object_id = obj.object_id                      INNER JOIN sys.schemas sch ON obj.schema_id = sch.schema_id             WHERE obj.type = 'U' -- Only user tables               AND sch.name = @SchemaName               AND obj.name = @TableName               AND NOT EXISTS (SELECT 1                               FROM sys.indexes i                               WHERE i.object_id = s.object_id                                 AND i.index_id = s.stats_id);             IF @sql_updateStats IS NOT NULL                 EXEC sp_executesql @sql_updateStats;         end     else         Begin             IF OBJECT_ID('tempdb..#StatsWithBatch') IS NOT NULL                 DROP TABLE #StatsWithBatch;              CREATE TABLE #StatsWithBatch             (                 UpdateCommand NVARCHAR(MAX),                 BatchNumber   INT             )             INSERT INTO #StatsWithBatch (UpdateCommand, BatchNumber)             SELECT 'UPDATE STATISTICS ' + QUOTENAME(sch.name) + '.' + QUOTENAME(obj.name)                        + ' ' + QUOTENAME(s.name) + ' WITH FULLSCAN;' AS UpdateCommand,                    NTILE(3) OVER (ORDER BY s.stats_id) AS BatchNumber -- Divide work into 2 batches             FROM sys.stats s                      INNER JOIN sys.objects obj ON s.object_id = obj.object_id                      INNER JOIN sys.schemas sch ON obj.schema_id = sch.schema_id             WHERE obj.type = 'U' -- Only user tables               AND sch.name = @SchemaName               AND obj.name = @TableName               AND NOT EXISTS (SELECT 1                               FROM sys.indexes i                               WHERE i.object_id = s.object_id                                 AND i.index_id = s.stats_id);             SELECT @Batch1_SQL = STRING_AGG(CAST(UpdateCommand AS NVARCHAR(MAX)), CHAR(13))             FROM #StatsWithBatch             WHERE BatchNumber = 1;              IF (@Batch1_SQL IS NOT NULL AND LEN(@Batch1_SQL) > 0)                 BEGIN                     PRINT '--- EXECUTING BATCH 1 ---';                     PRINT @Batch1_SQL;                     EXEC sp_executesql @Batch1_SQL;                     PRINT '--- BATCH 1 COMPLETE ---';                 END             ELSE                 BEGIN                     PRINT '--- BATCH 1: No commands to execute. ---';                 END              SELECT @Batch2_SQL = STRING_AGG(CAST(UpdateCommand AS NVARCHAR(MAX)), CHAR(13))             FROM #StatsWithBatch             WHERE BatchNumber = 2;              IF (@Batch2_SQL IS NOT NULL AND LEN(@Batch2_SQL) > 0)                 BEGIN                     PRINT '--- EXECUTING BATCH 2 ---';                     PRINT @Batch2_SQL;                     exec sp_executesql @Batch2_SQL;                     PRINT '--- BATCH 2 COMPLETE ---';                 END             ELSE                 BEGIN                     PRINT '--- BATCH 2: No commands to execute. ---';                 END             SELECT @Batch3_SQL = STRING_AGG(CAST(UpdateCommand AS NVARCHAR(MAX)), CHAR(13))             FROM #StatsWithBatch             WHERE BatchNumber = 3;              IF (@Batch3_SQL IS NOT NULL AND LEN(@Batch3_SQL) > 0)                 BEGIN                     PRINT '--- EXECUTING BATCH 2 ---';                     PRINT @Batch3_SQL;                     exec sp_executesql @Batch3_SQL;                     PRINT '--- BATCH 3 COMPLETE ---';                 END             ELSE                 BEGIN                     PRINT '--- BATCH 3: No commands to execute. ---';                 END  DROP TABLE #StatsWithBatch;         END          UPDATE [DatabaseName].DBA.ReindexJobLog     SET EndTime = GETDATE()     WHERE LogID = @LogID       and Tablename = @FullTableName END 

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

Шаг №1 — Создает динамические подзадания в количестве, равном числу потоков.

🚨Не забудьте изменить название базы данных в скрипте

Create PROCEDURE [DBA].[HotTablesReindexingJobCreating] @DBName Nvarchar(125), @TotalGroups int  AS BEGIN -- Cоздать список горячих таблиц  IF OBJECT_ID('tempdb..#HotTables') IS NOT NULL DROP TABLE #HotTables; SELECT      ROW_NUMBER() OVER(ORDER BY (SELECT 1)) AS RowNum,     QUOTENAME(SCHEMA_NAME(t.schema_id)) + '.' + QUOTENAME(t.name) AS FullTableName,     t.name AS TableName,     SCHEMA_NAME(t.schema_id) AS SchemaName INTO #HotTables FROM sys.tables t CROSS APPLY fn_listextendedproperty('IsHotTable', 'SCHEMA', SCHEMA_NAME(t.schema_id), 'TABLE', t.name, NULL, NULL) ep WHERE ep.value = '1'   AND t.is_ms_shipped = 0;   IF NOT EXISTS (SELECT * FROM #HotTables) BEGIN     PRINT 'No hot tables found.';     RETURN; END  -- Определить сколько потоков DECLARE @i INT = 1;  WHILE @i <= @TotalGroups BEGIN     DECLARE @JobName NVARCHAR(128) = @DBName + N'_Reindex_HotTables_Group_' + CAST(@i AS NVARCHAR(5));     DECLARE @SqlCommand NVARCHAR(MAX);     -- Создание динамических заданий     SET @SqlCommand = '';     SELECT @SqlCommand = @SqlCommand +          'EXEC [DatabaseName].dba.ReindexTableWithLogging @JobName = ''' + @JobName + ''', @GroupNumber = ' + CAST(@i AS NVARCHAR(10)) +', @DBName = ''' + @DBName + ''' , @TableName = ''' + t.TableName + ''', @SchemaName = ''' +t.SchemaName + ''';' + CHAR(13)+CHAR(10)     FROM #HotTables t     WHERE t.RowNum % @TotalGroups = @i - 1;           IF EXISTS (SELECT job_id FROM msdb.dbo.sysjobs WHERE name = @JobName)         EXEC msdb.dbo.sp_delete_job @job_name = @JobName;           EXEC msdb.dbo.sp_add_job         @job_name = @JobName,         @enabled = 1,         @description = 'Auto-generated reindex job using stored procedure',         @category_name = 'Database Maintenance';           EXEC msdb.dbo.sp_add_jobstep         @job_name = @JobName,         @step_name = N'Reindex Tables',         @subsystem = N'TSQL', @database_name = @DBName,         @command = @SqlCommand,         @retry_attempts = 0,         @retry_interval = 0;         EXEC msdb.dbo.sp_add_jobschedule         @job_name = @JobName,         @name = 'RunOnceNow',         @freq_type = 1,         @active_start_time = 0;     EXEC msdb.dbo.sp_add_jobserver         @job_name = @JobName,         @server_name = N'(local)';     -- запуск     EXEC msdb.dbo.sp_start_job @job_name = @JobName;     SET @i = @i + 1; END END 

Шаг №2 — отслеживает созданные подзадания и удаляет их после выполнения

Create proc [DBA].[CleaningReindexingJob] @DBName Nvarchar(128) as begin  DECLARE @JobName NVARCHAR(128); DECLARE @i INT; DECLARE @IsRunning BIT; DECLARE @WaitInterval CHAR(8) = '00:30:00'; -- 30-minute wait interval   WAITFOR DELAY @WaitInterval; ---------------------------------------------------------------------- -- Section 1: Process the 'Hot' table jobs (1 to 3) ---------------------------------------------------------------------- PRINT '--- Checking HOT Table Jobs ---'; SET @i = 1;  WHILE @i <= 3 BEGIN     SET @JobName = @DBName + '_Reindex_HotTables_Group_' + CAST(@i AS NVARCHAR(5));      IF EXISTS (SELECT 1 FROM msdb.dbo.sysjobs WHERE name = @JobName)     BEGIN         PRINT 'Checking job: ' + @JobName;         SET @IsRunning = 1;          WHILE @IsRunning = 1         BEGIN             SELECT @IsRunning = COUNT(*)             FROM msdb.dbo.sysjobactivity ja             JOIN msdb.dbo.sysjobs j ON ja.job_id = j.job_id             WHERE j.name = @JobName AND ja.start_execution_date IS NOT NULL AND ja.stop_execution_date IS NULL;              IF @IsRunning = 1             BEGIN                 PRINT 'Job ' + @JobName + ' is still running. Waiting for 30 minutes...';                 WAITFOR DELAY @WaitInterval;             END         END          EXEC msdb.dbo.sp_delete_job @job_name = @JobName;         PRINT '✅ Job deleted: ' + @JobName;     END     ELSE     BEGIN         PRINT '❌ Job does not exist: ' + @JobName;     END      SET @i = @i + 1; END PRINT '--- Finished checking HOT Table Jobs ---'; PRINT ''; -- Add a blank line for readability  ---------------------------------------------------------------------- -- Section 2: Process the 'Warm' table jobs (1 to 10) ---------------------------------------------------------------------- PRINT '--- Checking WARM Table Jobs ---'; SET @i = 1; -- Important: Reset the counter to 1  WHILE @i <= 10 BEGIN     SET @JobName = @DBName + '_Reindex_WarmTables_Group_' + CAST(@i AS NVARCHAR(5));      IF EXISTS (SELECT 1 FROM msdb.dbo.sysjobs WHERE name = @JobName)     BEGIN         PRINT 'Checking job: ' + @JobName;         SET @IsRunning = 1;          WHILE @IsRunning = 1         BEGIN             SELECT @IsRunning = COUNT(*)             FROM msdb.dbo.sysjobactivity ja             JOIN msdb.dbo.sysjobs j ON ja.job_id = j.job_id             WHERE j.name = @JobName AND ja.start_execution_date IS NOT NULL AND ja.stop_execution_date IS NULL;              IF @IsRunning = 1             BEGIN                 PRINT 'Job ' + @JobName + ' is still running. Waiting for 30 minutes...';                 WAITFOR DELAY @WaitInterval;             END         END          EXEC msdb.dbo.sp_delete_job @job_name = @JobName;         PRINT '✅ Job deleted: ' + @JobName;     END     ELSE     BEGIN         PRINT '❌ Job does not exist: ' + @JobName;     END      SET @i = @i + 1; END PRINT '--- Finished checking WARM Table Jobs ---'; ENd GO 

Обслуживание теплых таблиц

Для теплых таблиц предусмотрена ежедневная переиндексация на основе уровня фрагментации:

Уровень фрагментации

Действие

< 5%

Обновление статистики

5% — 30%

Реорганизация (Reorganize) индексов

≥ 30%

Перестроение (Rebuild) индексов

Дополнительно планируется еженедельное перестроение (Rebuild) индексов вне зависимости от уровня фрагментации.

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

🚨Не забудьте изменить название базы данных в скрипте

SET ANSI_NULLS ON GO  SET QUOTED_IDENTIFIER ON GO  CREATE PROCEDURE [DBA].[ReindexTableWithStats] @DBName Nvarchar(128),                                               @SchemaName SYSNAME,                                               @TableName SYSNAME,                                               @JobName NVARCHAR(128),                                               @GroupNumber INT AS BEGIN     SET NOCOUNT ON;     DECLARE @IndexName SYSNAME;     DECLARE @IndexType NVARCHAR(150);     DECLARE @Fragmentation FLOAT;     DECLARE @SQL NVARCHAR(MAX);     DECLARE @Action NVARCHAR(50);     Declare @sql_updateStats NVARCHAR(MAX);     DECLARE @Batch1_SQL NVARCHAR(MAX);     DECLARE @Batch2_SQL NVARCHAR(MAX);     DECLARE @Batch3_SQL NVARCHAR(MAX);     -- Cursor to loop through indexes of the table     DECLARE IndexCursor CURSOR LOCAL FAST_FORWARD FOR         SELECT i.name AS IndexName,                ips.index_type_desc,                ips.avg_fragmentation_in_percent         FROM sys.dm_db_index_physical_stats(DB_ID(), OBJECT_ID(QUOTENAME(@SchemaName) + '.' + QUOTENAME(@TableName)),                                             NULL, NULL, 'LIMITED') ips                  JOIN              sys.indexes i ON ips.object_id = i.object_id AND ips.index_id = i.index_id         WHERE i.name IS NOT NULL -- Exclude heaps           and ips.alloc_unit_type_desc = 'IN_ROW_DATA';      OPEN IndexCursor;     FETCH NEXT FROM IndexCursor INTO @IndexName, @IndexType, @Fragmentation;      WHILE @@FETCH_STATUS = 0         BEGIN             -- Determine action based on fragmentation             IF @Fragmentation < 5                 BEGIN                     SET @Action = 'Update Statistics Only';                     SET @SQL = 'UPDATE STATISTICS ' + QUOTENAME(@SchemaName) + '.' + QUOTENAME(@TableName) + ' ' +                                QUOTENAME(@IndexName) + ' with FullScan;';                 END             ELSE                 IF @Fragmentation BETWEEN 5 AND 30                     BEGIN                         SET @Action = 'Reorganize + Update Statistics';                         SET @SQL = 'ALTER INDEX ' + QUOTENAME(@IndexName) + ' ON ' + QUOTENAME(@SchemaName) + '.' +                                    QUOTENAME(@TableName) + ' REORGANIZE;                         UPDATE STATISTICS ' + QUOTENAME(@SchemaName) + '.' + QUOTENAME(@TableName) + ' ' +                                    QUOTENAME(@IndexName) + ' with FullScan;';                     END                 ELSE                     BEGIN                         SET @Action = 'Rebuild';                         -- Use DBCC DBREINDEX instead of ALTER INDEX                         SET @SQL = 'DBCC DBREINDEX(''' + QUOTENAME(@SchemaName) + '.' + QUOTENAME(@TableName) + ''', ' +                                    QUOTENAME(@IndexName) + ', 90);';                     END              -- Log start of action             INSERT INTO [DatabaseName].dba.ReindexJobLog (JobName, GroupNumber, DBName, TableName, IndexName, ActionType,                                                    StartTime)             VALUES (@JobName, @GroupNumber, @DBName, QUOTENAME(@SchemaName) + '.' + QUOTENAME(@TableName), @IndexName,                     @Action, GETDATE());              DECLARE @LogID INT = SCOPE_IDENTITY();              PRINT 'Starting: ' + @Action + ' on ' + @SchemaName + '.' + @TableName + '.' + @IndexName;              BEGIN TRY                 EXEC sp_executesql @SQL;                 PRINT 'Finished: ' + @Action + ' on ' + @SchemaName + '.' + @TableName + '.' + @IndexName;                  -- Log end time                 UPDATE [DatabaseName].dba.ReindexJobLog                 SET EndTime = GETDATE()                 WHERE LogID = @LogID;             END TRY             BEGIN CATCH                 PRINT 'Error occurred during: ' + @Action + ' on ' + @SchemaName + '.' + @TableName + '.' + @IndexName;                 PRINT ERROR_MESSAGE();                  UPDATE [DatabaseName].dba.ReindexJobLog                 SET EndTime = GETDATE()                 WHERE LogID = @LogID;             END CATCH              FETCH NEXT FROM IndexCursor INTO @IndexName, @IndexType, @Fragmentation;         END     CLOSE IndexCursor;     DEALLOCATE IndexCursor;      --Update the rest of statistics     INSERT INTO [DatabaseName].dba.ReindexJobLog (JobName, GroupNumber, DBName, Tablename, Indexname, ActionType, StartTime)     VALUES (@JobName, @GroupNumber, @DBName, QUOTENAME(@SchemaName) + '.' + QUOTENAME(@TableName), '',             'Update rest of statistics', GETDATE());     SET @LogID = SCOPE_IDENTITY();     if         (SELECT count(1)          FROM sys.stats s                   INNER JOIN sys.objects obj ON s.object_id = obj.object_id                   INNER JOIN sys.schemas sch ON obj.schema_id = sch.schema_id          WHERE obj.type = 'U' -- Only user tables            AND sch.name = @SchemaName            AND obj.name = @TableName            AND NOT EXISTS (SELECT 1                            FROM sys.indexes i                            WHERE i.object_id = s.object_id                              AND i.index_id = s.stats_id)) < 20         Begin              SELECT @sql_updateStats = STRING_AGG(                     'UPDATE STATISTICS ' + QUOTENAME(sch.name) + '.' + QUOTENAME(obj.name)                         + ' ' + QUOTENAME(s.name) + ' WITH FULLSCAN;',                     CHAR(13)                                       )             FROM sys.stats s                      INNER JOIN sys.objects obj ON s.object_id = obj.object_id                      INNER JOIN sys.schemas sch ON obj.schema_id = sch.schema_id             WHERE obj.type = 'U' -- Only user tables               AND sch.name = @SchemaName               AND obj.name = @TableName               AND NOT EXISTS (SELECT 1                               FROM sys.indexes i                               WHERE i.object_id = s.object_id                                 AND i.index_id = s.stats_id);             IF @sql_updateStats IS NOT NULL                 EXEC sp_executesql @sql_updateStats;         end     else         Begin             IF OBJECT_ID('tempdb..#StatsWithBatch') IS NOT NULL                 DROP TABLE #StatsWithBatch;              CREATE TABLE #StatsWithBatch             (                 UpdateCommand NVARCHAR(MAX),                 BatchNumber   INT             )             INSERT INTO #StatsWithBatch (UpdateCommand, BatchNumber)             SELECT 'UPDATE STATISTICS ' + QUOTENAME(sch.name) + '.' + QUOTENAME(obj.name)                        + ' ' + QUOTENAME(s.name) + ' WITH FULLSCAN;' AS UpdateCommand,                    NTILE(3) OVER (ORDER BY s.stats_id) AS BatchNumber -- Divide work into 2 batches             FROM sys.stats s                      INNER JOIN sys.objects obj ON s.object_id = obj.object_id                      INNER JOIN sys.schemas sch ON obj.schema_id = sch.schema_id             WHERE obj.type = 'U' -- Only user tables               AND sch.name = @SchemaName               AND obj.name = @TableName               AND NOT EXISTS (SELECT 1                               FROM sys.indexes i                               WHERE i.object_id = s.object_id                                 AND i.index_id = s.stats_id);             SELECT @Batch1_SQL = STRING_AGG(CAST(UpdateCommand AS NVARCHAR(MAX)), CHAR(13))             FROM #StatsWithBatch             WHERE BatchNumber = 1;              IF (@Batch1_SQL IS NOT NULL AND LEN(@Batch1_SQL) > 0)                 BEGIN                     PRINT '--- EXECUTING BATCH 1 ---';                     PRINT @Batch1_SQL;                     EXEC sp_executesql @Batch1_SQL;                     PRINT '--- BATCH 1 COMPLETE ---';                 END             ELSE                 BEGIN                     PRINT '--- BATCH 1: No commands to execute. ---';                 END              SELECT @Batch2_SQL = STRING_AGG(CAST(UpdateCommand AS NVARCHAR(MAX)), CHAR(13))             FROM #StatsWithBatch             WHERE BatchNumber = 2;              IF (@Batch2_SQL IS NOT NULL AND LEN(@Batch2_SQL) > 0)                 BEGIN                     PRINT '--- EXECUTING BATCH 2 ---';                     PRINT @Batch2_SQL;                     exec sp_executesql @Batch2_SQL;                     PRINT '--- BATCH 2 COMPLETE ---';                 END             ELSE                 BEGIN                     PRINT '--- BATCH 2: No commands to execute. ---';                 END             SELECT @Batch3_SQL = STRING_AGG(CAST(UpdateCommand AS NVARCHAR(MAX)), CHAR(13))             FROM #StatsWithBatch             WHERE BatchNumber = 3;              IF (@Batch3_SQL IS NOT NULL AND LEN(@Batch3_SQL) > 0)                 BEGIN                     PRINT '--- EXECUTING BATCH 2 ---';                     PRINT @Batch3_SQL;                     exec sp_executesql @Batch3_SQL;                     PRINT '--- BATCH 3 COMPLETE ---';                 END             ELSE                 BEGIN                     PRINT '--- BATCH 3: No commands to execute. ---';                 END DROP TABLE #StatsWithBatch;         END           UPDATE [DatabaseName].DBA.ReindexJobLog     SET EndTime = GETDATE()     WHERE LogID = @LogID       and Tablename = QUOTENAME(@SchemaName) + '.' + QUOTENAME(@TableName) END GO

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

Хранимая процедура для создания задания по запуску переиндексации теплых таблиц

Create PROCEDURE [DBA].[WarmTablesReindexingJobCreating] @DBName Nvarchar(125), @TotalGroups int AS BEGIN -- Cоздать список таблиц  IF OBJECT_ID('tempdb..#warmTables') IS NOT NULL DROP TABLE #warmTables; SELECT      ROW_NUMBER() OVER(ORDER BY (SELECT 1)) AS RowNum,     QUOTENAME(SCHEMA_NAME(t.schema_id)) + '.' + QUOTENAME(t.name) AS FullTableName,     t.name AS TableName,     SCHEMA_NAME(t.schema_id) AS SchemaName INTO #warmTables FROM sys.tables t inner join    sys.schemas s ON t.schema_id = s.schema_id WHERE      NOT EXISTS (         SELECT 1         FROM fn_listextendedproperty('IsHotTable', 'SCHEMA', s.name, 'TABLE', t.name, NULL, NULL) ep         WHERE ep.value = '1'     ) and t.name not like 'X_%'   IF NOT EXISTS (SELECT * FROM #warmTables) BEGIN     PRINT 'No hot tables found.';     RETURN; END  -- Определить сколько потоков  DECLARE @i INT = 1;   WHILE @i <= @TotalGroups BEGIN     DECLARE @JobName NVARCHAR(128) = @DBName +N'_Reindex_WarmTables_Group_' + CAST(@i AS NVARCHAR(5));     DECLARE @SqlCommand NVARCHAR(MAX);      -- Создание динамических заданий     SET @SqlCommand = '';       SELECT @SqlCommand = @SqlCommand +          'EXEC [DatabaseName].dba.ReindexTableWithStats @JobName = ''' + @JobName + ''', @GroupNumber = ' + CAST(@i AS NVARCHAR(10)) + ', @DBName = ''' + @DBName + '''' + ', @TableName = ''' + t.TableName + ''', @SchemaName = ''' +t.SchemaName + ''';' + CHAR(13)+CHAR(10)     FROM #warmTables t     WHERE t.RowNum % @TotalGroups = @i - 1;           IF EXISTS (SELECT job_id FROM msdb.dbo.sysjobs WHERE name = @JobName)         EXEC msdb.dbo.sp_delete_job @job_name = @JobName;           EXEC msdb.dbo.sp_add_job         @job_name = @JobName,         @enabled = 1,         @description = 'Auto-generated reindex job using stored procedure',         @category_name = 'Database Maintenance';           EXEC msdb.dbo.sp_add_jobstep         @job_name = @JobName,         @step_name = N'Reindex Tables',         @subsystem = N'TSQL', @database_name = @DBName,         @command = @SqlCommand,         @retry_attempts = 0,         @retry_interval = 0;           EXEC msdb.dbo.sp_add_jobschedule         @job_name = @JobName,         @name = 'RunOnceNow',         @freq_type = 1,         @active_start_time = 0;           EXEC msdb.dbo.sp_add_jobserver         @job_name = @JobName,         @server_name = N'(local)';      -- запуск    EXEC msdb.dbo.sp_start_job @job_name = @JobName;      SET @i = @i + 1; END END 

🚧 Было бы хорошо, если бы имелась возможность еженедельно переиндексировать все таблицы с помощью ХП ReindexTableWithLogging

Для упрощения работы администраторов СУБД, я объединил всю систему в единый скрипт, который автоматически создает задание в SQL Agent Job

USE [msdb] GO  /****** Object:  Job [Smart_Reindex_]    Script Date: 20.08.2025 17:23:15 ******/ BEGIN TRANSACTION DECLARE @ReturnCode INT SELECT @ReturnCode = 0 /****** Object:  JobCategory [[Uncategorized (Local)]]    Script Date: 20.08.2025 17:23:16 ******/ IF NOT EXISTS (SELECT name FROM msdb.dbo.syscategories WHERE name=N'[Uncategorized (Local)]' AND category_class=1) BEGIN EXEC @ReturnCode = msdb.dbo.sp_add_category @class=N'JOB', @type=N'LOCAL', @name=N'[Uncategorized (Local)]' IF (@@ERROR <> 0 OR @ReturnCode <> 0) GOTO QuitWithRollback  END  DECLARE @jobId BINARY(16) EXEC @ReturnCode =  msdb.dbo.sp_add_job @job_name=N'Smart_Reindex_',  @enabled=1,  @notify_level_eventlog=0,  @notify_level_email=0,  @notify_level_netsend=0,  @notify_level_page=0,  @delete_level=0,  @description=N'No description available.',  @category_name=N'[Uncategorized (Local)]',  @owner_login_name=N'sa', @job_id = @jobId OUTPUT IF (@@ERROR <> 0 OR @ReturnCode <> 0) GOTO QuitWithRollback /****** Object:  Step [HotTable reindex]    Script Date: 20.08.2025 17:23:16 ******/ EXEC @ReturnCode = msdb.dbo.sp_add_jobstep @job_id=@jobId, @step_name=N'HotTable reindex',  @step_id=1,  @cmdexec_success_code=0,  @on_success_action=3,  @on_success_step_id=0,  @on_fail_action=2,  @on_fail_step_id=0,  @retry_attempts=0,  @retry_interval=0,  @os_run_priority=0, @subsystem=N'TSQL',  @command=N'Exec [DatabaseName].DBA.HotTablesReindexingJobCreating @DBName = ''[DatabaseName]'', @TotalGroups = 5',  @database_name=N'[DatabaseName]',  @flags=0 IF (@@ERROR <> 0 OR @ReturnCode <> 0) GOTO QuitWithRollback /****** Object:  Step [WarmTables]    Script Date: 20.08.2025 17:23:16 ******/ EXEC @ReturnCode = msdb.dbo.sp_add_jobstep @job_id=@jobId, @step_name=N'WarmTables',  @step_id=2,  @cmdexec_success_code=0,  @on_success_action=3,  @on_success_step_id=0,  @on_fail_action=3,  @on_fail_step_id=0,  @retry_attempts=0,  @retry_interval=0,  @os_run_priority=0, @subsystem=N'TSQL',  @command=N'Exec [DatabaseName].DBA.WarmTablesReindexingJobCreating @DBName = ''[DatabaseName]'', @TotalGroups = 15',  @database_name=N'[DatabaseName]',  @flags=0 IF (@@ERROR <> 0 OR @ReturnCode <> 0) GOTO QuitWithRollback /****** Object:  Step [Clear_after_reindexing]    Script Date: 20.08.2025 17:23:16 ******/ EXEC @ReturnCode = msdb.dbo.sp_add_jobstep @job_id=@jobId, @step_name=N'Clear_after_reindexing',  @step_id=3,  @cmdexec_success_code=0,  @on_success_action=1,  @on_success_step_id=0,  @on_fail_action=2,  @on_fail_step_id=0,  @retry_attempts=0,  @retry_interval=0,  @os_run_priority=0, @subsystem=N'TSQL',  @command=N'Exec [DatabaseName].DBA.CleaningReindexingJob @DBName = ''[DatabaseName]''',  @database_name=N'[DatabaseName]',  @flags=0 IF (@@ERROR <> 0 OR @ReturnCode <> 0) GOTO QuitWithRollback EXEC @ReturnCode = msdb.dbo.sp_update_job @job_id = @jobId, @start_step_id = 1 IF (@@ERROR <> 0 OR @ReturnCode <> 0) GOTO QuitWithRollback EXEC @ReturnCode = msdb.dbo.sp_add_jobschedule @job_id=@jobId, @name=N'Weekly_mon_Fri_18-00',  @enabled=1,  @freq_type=8,  @freq_interval=63,  @freq_subday_type=1,  @freq_subday_interval=0,  @freq_relative_interval=0,  @freq_recurrence_factor=1,  @active_start_date=20250718,  @active_end_date=99991231,  @active_start_time=201000,  @active_end_time=235959,  @schedule_uid=N'b8daac26-30d1-4343-b6a8-3814669bdf83' IF (@@ERROR <> 0 OR @ReturnCode <> 0) GOTO QuitWithRollback EXEC @ReturnCode = msdb.dbo.sp_add_jobserver @job_id = @jobId, @server_name = N'(local)' IF (@@ERROR <> 0 OR @ReturnCode <> 0) GOTO QuitWithRollback COMMIT TRANSACTION GOTO EndSave QuitWithRollback:     IF (@@TRANCOUNT > 0) ROLLBACK TRANSACTION EndSave: GO   


ссылка на оригинал статьи https://habr.com/ru/articles/939786/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *