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.