From a7d38ac01f8b6b869135afa9ceb2b9d1bdcd817b Mon Sep 17 00:00:00 2001 From: Erik Darling <2136037+erikdarlingdata@users.noreply.github.com> Date: Fri, 27 Feb 2026 06:56:16 -0500 Subject: [PATCH] =?UTF-8?q?Rule=2013:=20Add=20GetRangeThroughConvert=20det?= =?UTF-8?q?ection=20for=20CONVERT/CAST=20on=20columns=20Rule=2014:=20Fix?= =?UTF-8?q?=20lazy=20spool=20threshold=20=E2=80=94=20warn=20when=20cache?= =?UTF-8?q?=20hits=20<=205x=20misses?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rule 13 now detects both GetRangeWithMismatchedTypes (type mismatch) and GetRangeThroughConvert (explicit CONVERT/CAST on column) with distinct messages for each. Rule 14 replaces the confusing rebinds*2 >= rewinds condition with a clearer model: warn when rewinds < rebinds*5 (cache not earning its overhead). Critical when rewinds < rebinds (net negative). Co-Authored-By: Claude Opus 4.6 --- Dashboard/Services/PlanAnalyzer.cs | 35 +++++++++++++++++++----------- Lite/Services/PlanAnalyzer.cs | 35 +++++++++++++++++++----------- 2 files changed, 44 insertions(+), 26 deletions(-) diff --git a/Dashboard/Services/PlanAnalyzer.cs b/Dashboard/Services/PlanAnalyzer.cs index 5df73609..a4058a04 100644 --- a/Dashboard/Services/PlanAnalyzer.cs +++ b/Dashboard/Services/PlanAnalyzer.cs @@ -351,40 +351,49 @@ private static void AnalyzeNode(PlanNode node, PlanStatement stmt) }); } - // Rule 13: Mismatched data types (GetRangeWithMismatchedTypes) - if (node.PhysicalOp == "Compute Scalar" && - !string.IsNullOrEmpty(node.DefinedValues) && - node.DefinedValues.Contains("GetRangeWithMismatchedTypes", StringComparison.OrdinalIgnoreCase)) + // Rule 13: Mismatched data types (GetRangeWithMismatchedTypes / GetRangeThroughConvert) + if (node.PhysicalOp == "Compute Scalar" && !string.IsNullOrEmpty(node.DefinedValues)) { - node.Warnings.Add(new PlanWarning + var hasMismatch = node.DefinedValues.Contains("GetRangeWithMismatchedTypes", StringComparison.OrdinalIgnoreCase); + var hasConvert = node.DefinedValues.Contains("GetRangeThroughConvert", StringComparison.OrdinalIgnoreCase); + + if (hasMismatch || hasConvert) { - WarningType = "Data Type Mismatch", - Message = "Implicit conversion due to mismatched data types. The column type does not match the parameter or literal type, forcing SQL Server to convert values at runtime. Fix the parameter type to match the column.", - Severity = PlanWarningSeverity.Warning - }); + var reason = hasMismatch + ? "Implicit conversion due to mismatched data types. The column type does not match the parameter or literal type, forcing SQL Server to convert values at runtime. Fix the parameter type to match the column." + : "Implicit conversion through CONVERT/CAST on a column. SQL Server must convert values at runtime, which can prevent index seeks. Remove the conversion or add a computed column."; + + node.Warnings.Add(new PlanWarning + { + WarningType = "Data Type Mismatch", + Message = reason, + Severity = PlanWarningSeverity.Warning + }); + } } // Rule 14: Lazy Table Spool unfavorable rebind/rewind ratio + // Rebinds = cache misses (child re-executes), rewinds = cache hits (reuse cached result) if (node.LogicalOp == "Lazy Spool") { var rebinds = node.HasActualStats ? (double)node.ActualRebinds : node.EstimateRebinds; var rewinds = node.HasActualStats ? (double)node.ActualRewinds : node.EstimateRewinds; var source = node.HasActualStats ? "actual" : "estimated"; - if (rebinds > 100 && (rewinds == 0 || rebinds * 2 >= rewinds)) + if (rebinds > 100 && rewinds < rebinds * 5) { - var severity = rebinds > rewinds + var severity = rewinds < rebinds ? PlanWarningSeverity.Critical : PlanWarningSeverity.Warning; var ratio = rewinds > 0 - ? $"{rewinds / rebinds:F1}x more rewinds (cache hits) than rebinds (cache misses)" + ? $"{rewinds / rebinds:F1}x rewinds (cache hits) per rebind (cache miss)" : "no rewinds (cache hits) at all"; node.Warnings.Add(new PlanWarning { WarningType = "Lazy Spool Ineffective", - Message = $"Lazy spool has unfavorable rebind/rewind ratio ({source}): {rebinds:N0} rebinds, {rewinds:N0} rewinds — {ratio}. The spool cache is not providing significant benefit.", + Message = $"Lazy spool has low cache hit ratio ({source}): {rebinds:N0} rebinds, {rewinds:N0} rewinds — {ratio}. The spool cache is not earning its overhead.", Severity = severity }); } diff --git a/Lite/Services/PlanAnalyzer.cs b/Lite/Services/PlanAnalyzer.cs index 803b443b..5760afaa 100644 --- a/Lite/Services/PlanAnalyzer.cs +++ b/Lite/Services/PlanAnalyzer.cs @@ -351,40 +351,49 @@ private static void AnalyzeNode(PlanNode node, PlanStatement stmt) }); } - // Rule 13: Mismatched data types (GetRangeWithMismatchedTypes) - if (node.PhysicalOp == "Compute Scalar" && - !string.IsNullOrEmpty(node.DefinedValues) && - node.DefinedValues.Contains("GetRangeWithMismatchedTypes", StringComparison.OrdinalIgnoreCase)) + // Rule 13: Mismatched data types (GetRangeWithMismatchedTypes / GetRangeThroughConvert) + if (node.PhysicalOp == "Compute Scalar" && !string.IsNullOrEmpty(node.DefinedValues)) { - node.Warnings.Add(new PlanWarning + var hasMismatch = node.DefinedValues.Contains("GetRangeWithMismatchedTypes", StringComparison.OrdinalIgnoreCase); + var hasConvert = node.DefinedValues.Contains("GetRangeThroughConvert", StringComparison.OrdinalIgnoreCase); + + if (hasMismatch || hasConvert) { - WarningType = "Data Type Mismatch", - Message = "Implicit conversion due to mismatched data types. The column type does not match the parameter or literal type, forcing SQL Server to convert values at runtime. Fix the parameter type to match the column.", - Severity = PlanWarningSeverity.Warning - }); + var reason = hasMismatch + ? "Implicit conversion due to mismatched data types. The column type does not match the parameter or literal type, forcing SQL Server to convert values at runtime. Fix the parameter type to match the column." + : "Implicit conversion through CONVERT/CAST on a column. SQL Server must convert values at runtime, which can prevent index seeks. Remove the conversion or add a computed column."; + + node.Warnings.Add(new PlanWarning + { + WarningType = "Data Type Mismatch", + Message = reason, + Severity = PlanWarningSeverity.Warning + }); + } } // Rule 14: Lazy Table Spool unfavorable rebind/rewind ratio + // Rebinds = cache misses (child re-executes), rewinds = cache hits (reuse cached result) if (node.LogicalOp == "Lazy Spool") { var rebinds = node.HasActualStats ? (double)node.ActualRebinds : node.EstimateRebinds; var rewinds = node.HasActualStats ? (double)node.ActualRewinds : node.EstimateRewinds; var source = node.HasActualStats ? "actual" : "estimated"; - if (rebinds > 100 && (rewinds == 0 || rebinds * 2 >= rewinds)) + if (rebinds > 100 && rewinds < rebinds * 5) { - var severity = rebinds > rewinds + var severity = rewinds < rebinds ? PlanWarningSeverity.Critical : PlanWarningSeverity.Warning; var ratio = rewinds > 0 - ? $"{rewinds / rebinds:F1}x more rewinds (cache hits) than rebinds (cache misses)" + ? $"{rewinds / rebinds:F1}x rewinds (cache hits) per rebind (cache miss)" : "no rewinds (cache hits) at all"; node.Warnings.Add(new PlanWarning { WarningType = "Lazy Spool Ineffective", - Message = $"Lazy spool has unfavorable rebind/rewind ratio ({source}): {rebinds:N0} rebinds, {rewinds:N0} rewinds — {ratio}. The spool cache is not providing significant benefit.", + Message = $"Lazy spool has low cache hit ratio ({source}): {rebinds:N0} rebinds, {rewinds:N0} rewinds — {ratio}. The spool cache is not earning its overhead.", Severity = severity }); }