Победа в тихой производительности: запланированное задание по обслуживанию индекса SQL в Optimizely

8 октября 2025 г.

Станислав Шолковский

Теги:


база данных
(1)


БД
(1)


эписервер
(8)


индекс
(1)


индексы
(1)


рабочие места
(3)


обслуживание
(2)


Оптимально
(8)


производительность
(1)


запланированные задания
(2)

По мере роста проектов Optimizely CMS нередко вводятся пользовательские таблицы — будь то для интеграции, кэширования или специализированной бизнес-логики. Но с хорошей схемой приходит и большая ответственность: индексы и статистика SQL Server тоже нуждаются в любви.

Хотя Optimizely хорошо обрабатывает собственные структуры данных, пользовательские таблицы могут незаметно снизить производительность, если их не контролировать. Optimizely Commerce включает встроенное задание для обслуживания индекса, но если ваше решение использует только CMS, эта функция будет отсутствовать. В этом посте я покажу, как автоматизировать обслуживание индекса и статистики с помощью запланированного задания.

Почему вас должно это волновать

SQL Server в значительной степени полагается на актуальную статистику и исправные индексы для оптимизации выполнения запросов. Фрагментированные индексы и устаревшая статистика могут привести к медленным запросам, увеличению использования ЦП и недовольству редакторов.

Если вы добавляете пользовательские таблицы в базу данных CMS, особенно те, которые со временем растут, вам следует подумать о регулярном обслуживании. А что может быть лучше, чем запланированное задание, которое тихо выполняется в фоновом режиме?

Запланированное задание

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



/// 
/// Automated database index maintenance job that runs on a schedule to optimize SQL Server performance.
/// This job analyzes index fragmentation and performs maintenance operations to keep queries running efficiently.
/// 
[ScheduledPlugIn(
    DisplayName = "Database Index Maintenance Scheduled Job",
    SortIndex = 20000)]
public sealed class DatabaseIndexMaintenanceScheduledJob : ScheduledJobBase
{
    private bool _stopRequested;
    private readonly IConfiguration _configuration;

    /// 
    /// Constructor injecting configuration for database connection access.
    /// Sets IsStoppable to allow manual termination of long-running maintenance operations.
    /// 
    public DatabaseIndexMaintenanceScheduledJob(IConfiguration configuration)
    {
        _configuration = configuration;

        // Allow administrators to stop the job if it's running too long
        IsStoppable = true;
    }

    /// 
    /// Handles stop requests by setting a flag that's checked during execution loops.
    /// This allows graceful cancellation between maintenance operations.
    /// 
    public override void Stop()
    {
        _stopRequested = true;
    }

    /// 
    /// Main entry point for the scheduled job execution.
    /// Retrieves the database connection string and delegates to ExecuteInternal.
    /// 
    public override string Execute()
    {
        // Get the connection string from configuration
        var connectionString = _configuration.GetConnectionString("EPiServerDB");

        // Validate connection string exists before proceeding
        var result = !string.IsNullOrEmpty(connectionString)
        ? ExecuteInternal(connectionString)
        : "Connection string is empty";

        return result;
    }

    /// 
    /// Core maintenance logic that analyzes and optimizes database indexes.
    /// Uses a three-phase approach:
    /// 1. Query all indexes and measure their fragmentation levels
    /// 2. Rebuild or reorganize indexes based on fragmentation thresholds
    /// 3. Update statistics for tables with fragmented indexes
    /// 
    private string ExecuteInternal(string connectionString)
    {
        // StringBuilder accumulates log messages for the job execution report
        var log = new StringBuilder();
        try
        {
            // Establish database connection using 'using' for automatic disposal
            using var conn = new SqlConnection(connectionString);
            conn.Open();

            log.AppendLine("Starting index maintenance...");

            // Query SQL Server's Dynamic Management Views (DMVs) to analyze index fragmentation
            // sys.dm_db_index_physical_stats provides fragmentation metrics for each index
            var indexQuery = @"
                SELECT OBJECT_SCHEMA_NAME(s.[object_id]) AS SchemaName,
                        OBJECT_NAME(s.[object_id]) AS TableName,
                        i.name AS IndexName,
                        s.avg_fragmentation_in_percent AS Frag
                FROM sys.dm_db_index_physical_stats(DB_ID(), NULL, NULL, NULL, 'LIMITED') s
                JOIN sys.indexes i
                    ON s.[object_id] = i.[object_id]
                    AND s.index_id = i.index_id
                WHERE i.type_desc IN ('CLUSTERED', 'NONCLUSTERED')
                    AND s.page_count > 100;"; // Only analyze indexes with more than 100 pages (800KB+)

            using var cmd = new SqlCommand(indexQuery, conn);
            using var reader = cmd.ExecuteReader();

            // Phase 1: Collect all indexes and their fragmentation metrics
            // Store in a list to avoid maintaining an open reader during maintenance operations
            var indexList = new List<(string Schema, string Table, string Index, double Fragmentation)>();
            while (reader.Read() && !_stopRequested)
            {
                var schema = reader.GetString(0);      // Schema name (e.g., "dbo")
                var table = reader.GetString(1);       // Table name
                var index = reader.GetString(2);       // Index name
                var frag = reader.GetDouble(3);        // Fragmentation percentage (0-100)

                indexList.Add((schema, table, index, frag));
            }

            // Close the reader before executing maintenance commands
            reader.Close();

            // Phase 2: Perform index maintenance based on fragmentation thresholds
            // Industry best practices: REBUILD > 30%, REORGANIZE 5-30%, do nothing < 5%
            foreach (var (schema, table, index, frag) in indexList)
            {
                // Check for stop request between each index operation
                if (_stopRequested) break;

                // Use pattern matching to determine the appropriate maintenance action
                var sql = frag switch
                {
                    // Severe fragmentation (>30%): REBUILD creates a new index from scratch
                    // Add optionally "WITH ONLINE = ON" which allows concurrent queries during rebuild (Enterprise Edition only)
                    > 30 => $"ALTER INDEX [{index}] ON [{schema}].[{table}] REBUILD;",

                    // Moderate fragmentation (5-30%): REORGANIZE defragments the leaf level
                    // This is always an online operation and requires less resources than rebuild
                    > 5 => $"ALTER INDEX [{index}] ON [{schema}].[{table}] REORGANIZE;",

                    // Low fragmentation (<5%): No action needed
                    _ => null
                };

                // Execute the maintenance command if an action was determined
                if (sql != null)
                {
                    log.AppendLine($"Maintaining index [{index}] on [{schema}].[{table}] - Fragmentation: {frag:F2}%");
                    using var alterCmd = new SqlCommand(sql, conn);
                    // Set a generous timeout for long-running queries
                    alterCmd.CommandTimeout = 180;
                    alterCmd.ExecuteNonQuery();
                }
            }

            // Phase 3: Update statistics for tables that had fragmented indexes
            // Statistics help the query optimizer make better execution plan decisions
            foreach (var (schema, table, _, frag) in indexList)
            {
                // Check for stop request between each statistics operation
                if (_stopRequested) break;

                // Only update statistics for tables with fragmentation > 5%
                // SAMPLE 50 PERCENT balances accuracy with execution time
                var sql = frag switch
                {
                    > 5 => $"UPDATE STATISTICS [{schema}].[{table}] WITH SAMPLE 50 PERCENT;",
                    _ => null
                };

                if (sql != null)
                {
                    log.AppendLine($"Updating statistics for [{schema}].[{table}]...");
                    using var statsCmd = new SqlCommand(sql, conn);
                    // Set a generous timeout for long-running queries
                    statsCmd.CommandTimeout = 180;
                    statsCmd.ExecuteNonQuery();
                    log.AppendLine("Statistics updated.");
                }
            }

            conn.Close();
        }
        catch (Exception ex)
        {
            // Log any errors that occur during maintenance
            log.AppendLine($"Error: {ex.Message}");
        }

        // Return the accumulated log as the job execution result
        return log.ToString();
    }
}
  

×


/// 
/// Automated database index maintenance job that runs on a schedule to optimize SQL Server performance.
/// This job analyzes index fragmentation and performs maintenance operations to keep queries running efficiently.
/// 
[ScheduledPlugIn(
    DisplayName = "Database Index Maintenance Scheduled Job",
    SortIndex = 20000)]
public sealed class DatabaseIndexMaintenanceScheduledJob : ScheduledJobBase
{
    private bool _stopRequested;
    private readonly IConfiguration _configuration;

    /// 
    /// Constructor injecting configuration for database connection access.
    /// Sets IsStoppable to allow manual termination of long-running maintenance operations.
    /// 
    public DatabaseIndexMaintenanceScheduledJob(IConfiguration configuration)
    {
        _configuration = configuration;

        // Allow administrators to stop the job if it's running too long
        IsStoppable = true;
    }

    /// 
    /// Handles stop requests by setting a flag that's checked during execution loops.
    /// This allows graceful cancellation between maintenance operations.
    /// 
    public override void Stop()
    {
        _stopRequested = true;
    }

    /// 
    /// Main entry point for the scheduled job execution.
    /// Retrieves the database connection string and delegates to ExecuteInternal.
    /// 
    public override string Execute()
    {
        // Get the connection string from configuration
        var connectionString = _configuration.GetConnectionString("EPiServerDB");

        // Validate connection string exists before proceeding
        var result = !string.IsNullOrEmpty(connectionString)
        ? ExecuteInternal(connectionString)
        : "Connection string is empty";

        return result;
    }

    /// 
    /// Core maintenance logic that analyzes and optimizes database indexes.
    /// Uses a three-phase approach:
    /// 1. Query all indexes and measure their fragmentation levels
    /// 2. Rebuild or reorganize indexes based on fragmentation thresholds
    /// 3. Update statistics for tables with fragmented indexes
    /// 
    private string ExecuteInternal(string connectionString)
    {
        // StringBuilder accumulates log messages for the job execution report
        var log = new StringBuilder();
        try
        {
            // Establish database connection using 'using' for automatic disposal
            using var conn = new SqlConnection(connectionString);
            conn.Open();

            log.AppendLine("Starting index maintenance...");

            // Query SQL Server's Dynamic Management Views (DMVs) to analyze index fragmentation
            // sys.dm_db_index_physical_stats provides fragmentation metrics for each index
            var indexQuery = @"
                SELECT OBJECT_SCHEMA_NAME(s.[object_id]) AS SchemaName,
                        OBJECT_NAME(s.[object_id]) AS TableName,
                        i.name AS IndexName,
                        s.avg_fragmentation_in_percent AS Frag
                FROM sys.dm_db_index_physical_stats(DB_ID(), NULL, NULL, NULL, 'LIMITED') s
                JOIN sys.indexes i
                    ON s.[object_id] = i.[object_id]
                    AND s.index_id = i.index_id
                WHERE i.type_desc IN ('CLUSTERED', 'NONCLUSTERED')
                    AND s.page_count > 100;"; // Only analyze indexes with more than 100 pages (800KB+)

            using var cmd = new SqlCommand(indexQuery, conn);
            using var reader = cmd.ExecuteReader();

            // Phase 1: Collect all indexes and their fragmentation metrics
            // Store in a list to avoid maintaining an open reader during maintenance operations
            var indexList = new List<(string Schema, string Table, string Index, double Fragmentation)>();
            while (reader.Read() && !_stopRequested)
            {
                var schema = reader.GetString(0);      // Schema name (e.g., "dbo")
                var table = reader.GetString(1);       // Table name
                var index = reader.GetString(2);       // Index name
                var frag = reader.GetDouble(3);        // Fragmentation percentage (0-100)

                indexList.Add((schema, table, index, frag));
            }

            // Close the reader before executing maintenance commands
            reader.Close();

            // Phase 2: Perform index maintenance based on fragmentation thresholds
            // Industry best practices: REBUILD > 30%, REORGANIZE 5-30%, do nothing < 5%
            foreach (var (schema, table, index, frag) in indexList)
            {
                // Check for stop request between each index operation
                if (_stopRequested) break;

                // Use pattern matching to determine the appropriate maintenance action
                var sql = frag switch
                {
                    // Severe fragmentation (>30%): REBUILD creates a new index from scratch
                    // Add optionally "WITH ONLINE = ON" which allows concurrent queries during rebuild (Enterprise Edition only)
                    > 30 => $"ALTER INDEX [{index}] ON [{schema}].[{table}] REBUILD;",

                    // Moderate fragmentation (5-30%): REORGANIZE defragments the leaf level
                    // This is always an online operation and requires less resources than rebuild
                    > 5 => $"ALTER INDEX [{index}] ON [{schema}].[{table}] REORGANIZE;",

                    // Low fragmentation (<5%): No action needed
                    _ => null
                };

                // Execute the maintenance command if an action was determined
                if (sql != null)
                {
                    log.AppendLine($"Maintaining index [{index}] on [{schema}].[{table}] - Fragmentation: {frag:F2}%");
                    using var alterCmd = new SqlCommand(sql, conn);
                    // Set a generous timeout for long-running queries
                    alterCmd.CommandTimeout = 180;
                    alterCmd.ExecuteNonQuery();
                }
            }

            // Phase 3: Update statistics for tables that had fragmented indexes
            // Statistics help the query optimizer make better execution plan decisions
            foreach (var (schema, table, _, frag) in indexList)
            {
                // Check for stop request between each statistics operation
                if (_stopRequested) break;

                // Only update statistics for tables with fragmentation > 5%
                // SAMPLE 50 PERCENT balances accuracy with execution time
                var sql = frag switch
                {
                    > 5 => $"UPDATE STATISTICS [{schema}].[{table}] WITH SAMPLE 50 PERCENT;",
                    _ => null
                };

                if (sql != null)
                {
                    log.AppendLine($"Updating statistics for [{schema}].[{table}]...");
                    using var statsCmd = new SqlCommand(sql, conn);
                    // Set a generous timeout for long-running queries
                    statsCmd.CommandTimeout = 180;
                    statsCmd.ExecuteNonQuery();
                    log.AppendLine("Statistics updated.");
                }
            }

            conn.Close();
        }
        catch (Exception ex)
        {
            // Log any errors that occur during maintenance
            log.AppendLine($"Error: {ex.Message}");
        }

        // Return the accumulated log as the job execution result
        return log.ToString();
    }
}
      

Вопросы производительности

Во время исполнения

Операции ПЕРЕСТРОЙКИ:

  • Высокая загрузка ЦП (скачок 50–80 % в течение 1–5 минут)
  • Блокирует таблицу (если ONLINE = ON в версии Enterprise)
  • Следует запускать во время периодов обслуживания.

РЕОРГАНИЗАЦИЯ операций:

  • Минимальное воздействие на процессор (10-20%)
  • Онлайн работа (без блокировки)
  • Безопасно работать в рабочее время

После выполнения

Типичные улучшения для баз данных с фрагментацией более 30 %:

  • Производительность запросов: на 15–40 % быстрее
  • Загрузка ЦП: снижение на 10–15 %.
  • Страничный ввод-вывод: сокращение на 20–30 %.

Примечание: Преимущества наиболее заметны в:

  • Таблицы с числом строк > 1 млн.
  • Запросы со сканированием таблицы/индекса
  • Отчеты и аналитические запросы

Что поддерживается?

Эта работа анализирует все индексы по всей вашей базе данных, включая:

Тип таблицы Примеры Поддерживается?
Ваши персонализированные таблицы CustomOrderCache, IntegrationLog Да
Оптимизировать ядро CMS tblContent, tblContentProperty, tblWorkContent Да
Торговые столы OrderGroup, Shipment, LineItem Да (если установлено)
Идентификация ASP.NET AspNetUsers, AspNetRoles Да

Это безопасно?

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

  • ВОССТАНОВЛЕНИЕ операций может ненадолго заблокировать таблицы в стандартной версии
  • Большие таблицы Optimizely (например, tblContentProperty) может занять несколько минут
  • На известных сайтах первый запуск может занять 10–20 минут.

Стоит ли фильтровать?

В целях безопасности производства рассмотрите возможность фильтрации только по пользовательским таблицам. если:

  • У вас очень большая база данных CMS (более 50 ГБ).
  • Вы используете SQL Server Standard Edition (без перестроений ОНЛАЙН).
  • Вы хотите свести к минимуму влияние окна обслуживания

Понимание обновлений статистики

UPDATE STATISTICS гарантирует, что оптимизатор запросов имеет точные данные о:

  • Количество строк
  • Распределение данных
  • Индексная селективность

SAMPLE 50 PERCENT вариант:

  • Быстрее, чем FULLSCAN
  • Достаточно точный для большинства сценариев
  • Использовать WITH FULLSCAN для критических таблиц при необходимости

Примечания

UPDATE STATISTICS гарантирует, что у планировщика запросов будут свежие данные для работы.

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

Предоставленное задание предназначено для ВСЕХ индексов в базе данных, включая основные таблицы Optimizely. Логику можно настроить так, чтобы она ориентировалась только на таблицы из белого списка, изменив индексный запрос:

    SELECT OBJECT_SCHEMA_NAME(s.[object_id]) AS SchemaName,
           OBJECT_NAME(s.[object_id]) AS TableName,
           i.name AS IndexName,
           s.avg_fragmentation_in_percent AS Frag
    FROM sys.dm_db_index_physical_stats(DB_ID(), NULL, NULL, NULL, 'LIMITED') s
    JOIN sys.indexes i ON s.[object_id] = i.[object_id] AND s.index_id = i.index_id
    WHERE i.type_desc IN ('CLUSTERED', 'NONCLUSTERED')
        AND s.page_count > 100
        AND OBJECT_NAME(s.[object_id]) IN (XXXX)

Запрос также можно настроить для фильтрации по схеме или префиксу.

Поиск неисправностей

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

  • Увеличьте таймаут команды SQL
  • Рассмотрите возможность запуска REORGANIZE для конкретных индексов, а не для ВСЕХ.

Ошибки разрешения:

  • Убедитесь, что идентификатор пула приложений db_ddladmin роль
  • Проверьте правила брандмауэра Azure SQL при использовании DXP.

Высокая загрузка ЦП во время выполнения:

  • Перейдите в непиковые часы
  • Добавлять WITH (ONLINE = ON) опция, если вы используете версию Enterprise

Краткое содержание

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

Если вы выполняете развертывание в DXP, убедитесь, что задание безопасно выполнять в рабочей среде и не мешает другим запланированным задачам. Обычно полезно запускать это задание еженедельно в часы низкого трафика.

Ещё по этой теме

Read more:  US Open: правящий чемпион Сабаленка шторм возвращается против Пегулы, чтобы снова достичь финала | Арина Сабаленка

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.