1759941206
2025-10-08 08:00:00
2025年10月8日
StanisławSzołkowskiによる
タグ:
Optimizely CMSプロジェクトが成長するにつれて、統合、キャッシュ、専門的なビジネスロジックなど、カスタムテーブルを導入することは珍しくありません。しかし、大きなスキーマには大きな責任があります。SQLサーバーのインデックスと統計にも愛が必要です。
最適化は独自のデータ構造を適切に処理しますが、カスタムテーブルは、チェックされていないままにすると静かにパフォーマンスを低下させることができます。 Optimizely Commerceにはインデックスメンテナンスのための組み込みジョブが含まれますが、ソリューションがCMSのみを使用する場合、この機能は欠落します。この投稿では、スケジュールされたジョブを使用してインデックスと統計のメンテナンスを自動化する方法を示します。
なぜあなたは気にするべきなのか
SQL Serverは、クエリ実行を最適化するために、最新の統計と健全なインデックスに大きく依存しています。断片化されたインデックスと古い統計は、クエリが遅くなり、CPU使用量の増加、不幸な編集者につながる可能性があります。
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();
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 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 ( 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();
}
}
パフォーマンスに関する考慮事項
実行中
操作の再構築:
- 高いCPU使用(1〜5分間の50〜80%のスパイク)
- テーブルをロックします(場合を除きます
ONLINE = ONエンタープライズエディションで) - メンテナンスウィンドウ中に実行する必要があります
操作の再編成:
- 最小限のCPU衝撃(10-20%)
- オンライン操作(ブロッキングなし)
- 営業時間中は安全に実行できます
実行後
30%以上の断片化によるデータベースの典型的な改善:
- クエリパフォーマンス:15-40%速い
- CPUの使用量:10〜15%の削減
- ページI/O:20〜30%の減少
注記: メリットは最も顕著です。
- > 1m列のテーブル
- テーブル/インデックススキャン付きのクエリ
- レポートと分析のクエリ
何が維持されますか?
このジョブは分析します すべてのインデックス 以下を含むデータベース全体を越えて
| テーブルタイプ | 例 | 維持されていますか? |
|---|---|---|
| カスタムテーブル | CustomOrderCache、 IntegrationLog |
はい |
| Optimizely CMSコア | tblContent、 tblContentProperty、 tblWorkContent |
はい |
| コマーステーブル | OrderGroup、 Shipment、 LineItem |
はい(インストールされている場合) |
| ASP.NET ID | AspNetUsers、 AspNetRoles |
はい |
これは安全ですか?
はい、一般的に。 インデックスメンテナンス操作は、すべてのテーブルで安全です。しかし:
- 操作を再構築します 標準エディションでテーブルを簡単にロックできます
- 大規模な最適化テーブル(次のように
tblContentProperty)数分かかる場合があります - 最初の実行は、確立されたサイトで10〜20分かかる場合があります
フィルタリングする必要がありますか?
生産の安全性については、カスタムテーブルのみへのフィルタリングを検討してください もし:
- 非常に大きなCMSデータベース(50GB+)があります
- SQL Server Standard Editionを使用しています(オンライン再構築なし)
- メンテナンスウィンドウの影響を最小限に抑えたいです
統計の更新の理解
UPDATE STATISTICS クエリOptimizerに正確なデータがあることを確認します。
- 行数
- データ分布
- インデックス選択性
SAMPLE 50 PERCENT オプション:
- フルカンよりも速い
- ほとんどのシナリオで十分に正確です
- 使用
WITH FULLSCAN必要に応じて重要なテーブル用
メモ
更新統計により、クエリプランナーが使用できる新しいデータを保証します。
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コマンドタイムアウトを増やします
- すべてではなく、特定のインデックスで実行することを実行することを検討してください
許可エラー:
- アプリプールのアイデンティティがあることを確認してください
db_ddladmin役割 - DXPを使用する場合は、Azure SQLファイアウォールルールを確認してください
実行中の高いCPU:
- オフピーク時間に移動します
- 追加
WITH (ONLINE = ON)エンタープライズエディションを使用する場合のオプション
まとめ
この種の仕事は、カスタムテーブルが頻繁に更新されるが、Optimizelyの内部メンテナンスルーチンではカバーされていない環境で特に役立ちます。それは大きなパフォーマンスの勝利をもたらすことができる小さな追加です。
DXPに展開している場合は、ジョブが生産で安全に実行され、他のスケジュールされたタスクに干渉しないことを確認してください。通常、交通時間の少ない時間にこの仕事を毎週実行するのは良いことです。
#静かなパフォーマンスが勝つSQLインデックスメンテナンスのスケジュールされたジョブが最適化