某高頻 OLTP 系統:每小時 400 萬交易。30 分鐘 SQL:BatchCompleted 跟蹤產生 200 萬行記錄。人工篩選「最慢 Top 10」毫無意義——因為千次/分的微小查詢累積才是真凶。傳統聚合失效原因:參數不同、空白/註釋差異、關鍵字同義詞導致同一邏輯查詢無法歸類。
對每條跟蹤記錄的 TextData 執行確定性轉換:
EXEC、EXECUTE@num@hex@strNULL 關鍵字 → @null/*...*/、--...dbo.Proc、Proc、[Proc] → 統一為 Proc| 原始跟蹤文本 (3 條) | 正規化後模板 |
|---|---|
Proc_A 123, 'alpha', 0x0a9fff8b | Proc_A @num, @str, @hex |
EXECUTE [Proc_A] -345, 'beta', null /*comment*/ | |
EXEC dbo.Proc_A 9876543, default -- test |
以正規化模板為 Key,計算彙總指標:
SELECT
NormalizedTemplate AS QueryPattern,
COUNT(*) AS ExecutionCount,
SUM(Duration) / 1000.0 AS TotalDuration_ms,
AVG(Duration) / 1000.0 AS AvgDuration_ms,
SUM(CPU) AS TotalCPU,
AVG(CPU) AS AvgCPU,
SUM(Reads) AS TotalReads,
AVG(Reads) AS AvgReads,
SUM(Writes) AS TotalWrites,
MAX(Duration) / 1000.0 AS MaxDuration_ms
FROM CleanedTrace
GROUP BY NormalizedTemplate
ORDER BY TotalDuration_ms DESC; -- 或 TotalCPU, TotalReads 視瓶頸類型
| QueryPattern | ExecCount | AvgDur(ms) | TotalDur(ms) | AvgReads | 佔比 |
|---|---|---|---|---|---|
Proc_GetUserProfile @num | 48,231 | 12.3 | 593,241 | 1,245 | 38% |
Proc_UpdateInventory @num, @num | 15,672 | 45.8 | 717,778 | 3,892 | 46% |
SELECT * FROM Orders WHERE OrderID = @num | 210,445 | 0.8 | 168,356 | 42 | 11% |
洞察:Proc_UpdateInventory 單次較慢但總耗時最高;Proc_GetUserProfile 極高頻累積佔比 38%。優化這兩支即可解決 84% 壓力。
-- 步驟 1:建立伺服器端跟蹤 (低負載)
DECLARE @TraceID INT;
EXEC sp_trace_create @TraceID OUTPUT, 2, N'D:\Traces\OLTP_Baseline', 50, NULL; -- 50MB 滾動檔案
EXEC sp_trace_setevent @TraceID, 12, 1, 1; -- SQL:BatchCompleted, TextData
EXEC sp_trace_setevent @TraceID, 12, 13, 1; -- Duration
EXEC sp_trace_setevent @TraceID, 12, 14, 1; -- CPU
EXEC sp_trace_setevent @TraceID, 12, 16, 1; -- Reads
EXEC sp_trace_setevent @TraceID, 12, 17, 1; -- Writes
EXEC sp_trace_setevent @TraceID, 12, 3, 1; -- DatabaseID
EXEC sp_trace_setfilter @TraceID, 3, 0, 0, 5; -- 僅目標資料庫 DB_ID=5
EXEC sp_trace_setstatus @TraceID, 1; -- 啟動
-- 步驟 2:排程每小時停止、歸檔、重啟 (避免單檔過大)
核心邏輯建議用 SQLCLR 純量函數 封裝正規化演算法,效能比 T-SQL 字串操作快 10-50 倍:
-- SQLCLR C# 片段概念
[SqlFunction(IsDeterministic = true, IsPrecise = true)]
public static SqlString NormalizeSql(SqlString rawText)
{
if (rawText.IsNull) return SqlString.Null;
string sql = rawText.Value;
// 1. 移除註釋
sql = Regex.Replace(sql, @"/\*.*?\*/", "", RegexOptions.Singleline);
sql = Regex.Replace(sql, @"--.*$", "", RegexOptions.Multiline);
// 2. 關鍵字
sql = Regex.Replace(sql, @"\b(EXEC|EXECUTE)\b", "", RegexOptions.IgnoreCase);
// 3. 常值抽象化
sql = Regex.Replace(sql, @"\b\d+\b", "@num");
sql = Regex.Replace(sql, @"0x[0-9A-Fa-f]+", "@hex");
sql = Regex.Replace(sql, @"'[^']*'", "@str");
sql = Regex.Replace(sql, @"\bNULL\b", "@null");
// 4. 空白標準化
sql = Regex.Replace(sql, @"\s+", " ");
// 5. 物件名稱 (簡化版)
sql = Regex.Replace(sql, @"\[?\w+\]?\.", "");
return new SqlString(sql.Trim());
}
| 挑戰 | 對策 |
|---|---|
sp_executesql N'SELECT * FROM T WHERE ID = @p', N'@p int', 123 | 正規化時識別 sp_executesql 模式,提取第一參數作為模板主體 |
| 參數嗅探導致同模板多計畫 | 結合 sys.dm_exec_cached_plans 與 query_hash 進一步分組 |
| 臨時表/表變數名稱干擾 | 正規化階段移除 #temp, @table 宣告區塊,保留核心 DML |
| 資料量 | T-SQL 純量函數 | SQLCLR 函數 | Python (pandas) |
|---|---|---|---|
| 10 萬行 | 45 秒 | 1.2 秒 | 3.8 秒 |
| 100 萬行 | 9 分 | 14 秒 | 42 秒 |
| 200 萬行 (30 分鐘跟蹤) | 18 分 | 28 秒 | 85 秒 |
結論:生產環境必須用 SQLCLR 或外部流式處理 (Kafka Streams/Flink) 做清理,T-SQL 字串操作無法勝任百萬行級。
SET STATISTICS IO, TIME ON + 實際執行計畫分析query_hash) 自動分組,免去手工清理跟蹤清理工具的核心價值不在「清理」本身,而在於將非結構化跟蹤文本轉化為結構化查詢模板指標,使效能分析從「大海撈針」變為「儀表板巡檢」。掌握正規化演算法、SQLCLR 加速、自動化管道三大支柱,即可在任何 SQL Server 版本上建立可持續的效能洞察體系。