diff --git a/Dashboard/Services/PlanAnalyzer.cs b/Dashboard/Services/PlanAnalyzer.cs index 13f10f3d..2a378005 100644 --- a/Dashboard/Services/PlanAnalyzer.cs +++ b/Dashboard/Services/PlanAnalyzer.cs @@ -341,7 +341,9 @@ private static void AnalyzeNode(PlanNode node, PlanStatement stmt) } // Rule 11: Scan with residual predicate (skip if non-SARGable already flagged) - if (nonSargableReason == null && IsRowstoreScan(node) && !string.IsNullOrEmpty(node.Predicate)) + // A PROBE() alone is just a bitmap filter — not a real residual predicate. + if (nonSargableReason == null && IsRowstoreScan(node) && !string.IsNullOrEmpty(node.Predicate) && + !IsProbeOnly(node.Predicate)) { node.Warnings.Add(new PlanWarning { @@ -538,6 +540,25 @@ private static bool IsRowstoreScan(PlanNode node) !node.PhysicalOp.Contains("Columnstore", StringComparison.OrdinalIgnoreCase); } + /// + /// Returns true when the predicate contains ONLY PROBE() bitmap filter(s) + /// with no real residual predicate. PROBE alone is a bitmap filter pushed + /// down from a hash join — not interesting by itself. If a real predicate + /// exists alongside PROBE (e.g. "[col]=(1) AND PROBE(...)"), returns false. + /// + private static bool IsProbeOnly(string predicate) + { + // Strip all PROBE(...) expressions — PROBE args can contain nested parens + var stripped = Regex.Replace(predicate, @"PROBE\s*\([^()]*(?:\([^()]*\)[^()]*)*\)", "", + RegexOptions.IgnoreCase).Trim(); + + // Remove leftover AND/OR connectors and whitespace + stripped = Regex.Replace(stripped, @"\b(AND|OR)\b", "", RegexOptions.IgnoreCase).Trim(); + + // If nothing meaningful remains, it was PROBE-only + return stripped.Length == 0; + } + /// /// Returns true for any scan operator including columnstore. /// Excludes spools and constant scans. diff --git a/Lite/Services/PlanAnalyzer.cs b/Lite/Services/PlanAnalyzer.cs index efa9b234..ae1984e1 100644 --- a/Lite/Services/PlanAnalyzer.cs +++ b/Lite/Services/PlanAnalyzer.cs @@ -341,7 +341,9 @@ private static void AnalyzeNode(PlanNode node, PlanStatement stmt) } // Rule 11: Scan with residual predicate (skip if non-SARGable already flagged) - if (nonSargableReason == null && IsRowstoreScan(node) && !string.IsNullOrEmpty(node.Predicate)) + // A PROBE() alone is just a bitmap filter — not a real residual predicate. + if (nonSargableReason == null && IsRowstoreScan(node) && !string.IsNullOrEmpty(node.Predicate) && + !IsProbeOnly(node.Predicate)) { node.Warnings.Add(new PlanWarning { @@ -538,6 +540,25 @@ private static bool IsRowstoreScan(PlanNode node) !node.PhysicalOp.Contains("Columnstore", StringComparison.OrdinalIgnoreCase); } + /// + /// Returns true when the predicate contains ONLY PROBE() bitmap filter(s) + /// with no real residual predicate. PROBE alone is a bitmap filter pushed + /// down from a hash join — not interesting by itself. If a real predicate + /// exists alongside PROBE (e.g. "[col]=(1) AND PROBE(...)"), returns false. + /// + private static bool IsProbeOnly(string predicate) + { + // Strip all PROBE(...) expressions — PROBE args can contain nested parens + var stripped = Regex.Replace(predicate, @"PROBE\s*\([^()]*(?:\([^()]*\)[^()]*)*\)", "", + RegexOptions.IgnoreCase).Trim(); + + // Remove leftover AND/OR connectors and whitespace + stripped = Regex.Replace(stripped, @"\b(AND|OR)\b", "", RegexOptions.IgnoreCase).Trim(); + + // If nothing meaningful remains, it was PROBE-only + return stripped.Length == 0; + } + /// /// Returns true for any scan operator including columnstore. /// Excludes spools and constant scans.