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