Данный документ представляет собой стратегию по переиндексации таблиц в крупных базах данных (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/
Добавить комментарий