diff --git a/Dashboard/Services/PlanAnalyzer.cs b/Dashboard/Services/PlanAnalyzer.cs
index e96cf0c2..f49abb35 100644
--- a/Dashboard/Services/PlanAnalyzer.cs
+++ b/Dashboard/Services/PlanAnalyzer.cs
@@ -399,15 +399,16 @@ private static void AnalyzeNode(PlanNode node, PlanStatement stmt)
}
}
- // Rule 15: Join OR clause (Concatenation + Constant Scan under Merge Interval)
- // Pattern: Merge Interval → TopN Sort → Concatenation → Constant Scans
+ // Rule 15: Join OR clause (Concatenation + Constant Scan under join/Merge Interval)
+ // Best signal: Merge Interval → TopN Sort → Concatenation → Constant Scans
+ // Also fires under a join ancestor (broader catch)
if (node.PhysicalOp == "Concatenation")
{
var constantScanBranches = node.Children
.Count(c => c.PhysicalOp == "Constant Scan" ||
c.Children.Any(gc => gc.PhysicalOp == "Constant Scan"));
- if (constantScanBranches >= 2 && HasMergeIntervalAncestor(node))
+ if (constantScanBranches >= 2 && (HasMergeIntervalAncestor(node) || HasJoinAncestor(node)))
{
node.Warnings.Add(new PlanWarning
{
@@ -626,6 +627,22 @@ private static bool HasMergeIntervalAncestor(PlanNode node)
return false;
}
+ ///
+ /// Checks whether a node has any join ancestor.
+ ///
+ private static bool HasJoinAncestor(PlanNode node)
+ {
+ var ancestor = node.Parent;
+ while (ancestor != null)
+ {
+ if (ancestor.LogicalOp != null &&
+ ancestor.LogicalOp.Contains("Join", StringComparison.OrdinalIgnoreCase))
+ return true;
+ ancestor = ancestor.Parent;
+ }
+ return false;
+ }
+
///
/// Finds Sort and Hash Match operators in the tree that consume memory.
///
diff --git a/Lite/Services/PlanAnalyzer.cs b/Lite/Services/PlanAnalyzer.cs
index e9652625..f87f5be3 100644
--- a/Lite/Services/PlanAnalyzer.cs
+++ b/Lite/Services/PlanAnalyzer.cs
@@ -399,15 +399,16 @@ private static void AnalyzeNode(PlanNode node, PlanStatement stmt)
}
}
- // Rule 15: Join OR clause (Concatenation + Constant Scan under Merge Interval)
- // Pattern: Merge Interval → TopN Sort → Concatenation → Constant Scans
+ // Rule 15: Join OR clause (Concatenation + Constant Scan under join/Merge Interval)
+ // Best signal: Merge Interval → TopN Sort → Concatenation → Constant Scans
+ // Also fires under a join ancestor (broader catch)
if (node.PhysicalOp == "Concatenation")
{
var constantScanBranches = node.Children
.Count(c => c.PhysicalOp == "Constant Scan" ||
c.Children.Any(gc => gc.PhysicalOp == "Constant Scan"));
- if (constantScanBranches >= 2 && HasMergeIntervalAncestor(node))
+ if (constantScanBranches >= 2 && (HasMergeIntervalAncestor(node) || HasJoinAncestor(node)))
{
node.Warnings.Add(new PlanWarning
{
@@ -626,6 +627,22 @@ private static bool HasMergeIntervalAncestor(PlanNode node)
return false;
}
+ ///
+ /// Checks whether a node has any join ancestor.
+ ///
+ private static bool HasJoinAncestor(PlanNode node)
+ {
+ var ancestor = node.Parent;
+ while (ancestor != null)
+ {
+ if (ancestor.LogicalOp != null &&
+ ancestor.LogicalOp.Contains("Join", StringComparison.OrdinalIgnoreCase))
+ return true;
+ ancestor = ancestor.Parent;
+ }
+ return false;
+ }
+
///
/// Finds Sort and Hash Match operators in the tree that consume memory.
///