Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 38 additions & 10 deletions src/PlanViewer.App/Controls/PlanViewerControl.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1863,18 +1863,46 @@ private object BuildNodeTooltipContent(PlanNode node, List<PlanWarning>? allWarn
if (warnings != null && warnings.Count > 0)
{
stack.Children.Add(new Separator { Margin = new Thickness(0, 6, 0, 6) });
foreach (var w in warnings)

if (allWarnings != null)
{
var warnColor = w.Severity == PlanWarningSeverity.Critical ? "#E57373"
: w.Severity == PlanWarningSeverity.Warning ? "#FFB347" : "#6BB5FF";
stack.Children.Add(new TextBlock
// Root node: show distinct warning type names only
var distinct = warnings
.GroupBy(w => w.WarningType)
.Select(g => (Type: g.Key, MaxSeverity: g.Max(w => w.Severity), Count: g.Count()))
.OrderByDescending(g => g.MaxSeverity)
.ThenBy(g => g.Type);

foreach (var (type, severity, count) in distinct)
{
Text = $"\u26A0 {w.WarningType}: {w.Message}",
Foreground = new SolidColorBrush(Color.Parse(warnColor)),
FontSize = 11,
TextWrapping = TextWrapping.Wrap,
Margin = new Thickness(0, 2, 0, 0)
});
var warnColor = severity == PlanWarningSeverity.Critical ? "#E57373"
: severity == PlanWarningSeverity.Warning ? "#FFB347" : "#6BB5FF";
var label = count > 1 ? $"\u26A0 {type} ({count})" : $"\u26A0 {type}";
stack.Children.Add(new TextBlock
{
Text = label,
Foreground = new SolidColorBrush(Color.Parse(warnColor)),
FontSize = 11,
Margin = new Thickness(0, 2, 0, 0)
});
}
}
else
{
// Individual node: show full warning messages
foreach (var w in warnings)
{
var warnColor = w.Severity == PlanWarningSeverity.Critical ? "#E57373"
: w.Severity == PlanWarningSeverity.Warning ? "#FFB347" : "#6BB5FF";
stack.Children.Add(new TextBlock
{
Text = $"\u26A0 {w.WarningType}: {w.Message}",
Foreground = new SolidColorBrush(Color.Parse(warnColor)),
FontSize = 11,
TextWrapping = TextWrapping.Wrap,
Margin = new Thickness(0, 2, 0, 0)
});
}
}
}

Expand Down
6 changes: 5 additions & 1 deletion src/PlanViewer.Core/Services/PlanAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -861,7 +861,11 @@ _ when nonSargableReason.StartsWith("Function call") =>
}

// Rule 26: Row Goal (informational) — optimizer reduced estimate due to TOP/EXISTS/IN
if (!cfg.IsRuleDisabled(26) && node.EstimateRowsWithoutRowGoal > 0 && node.EstimateRows > 0 &&
// Only surface on data access operators (seeks/scans) where the row goal actually matters
var isDataAccess = node.PhysicalOp != null &&
(node.PhysicalOp.Contains("Scan") || node.PhysicalOp.Contains("Seek"));
if (!cfg.IsRuleDisabled(26) && isDataAccess &&
node.EstimateRowsWithoutRowGoal > 0 && node.EstimateRows > 0 &&
node.EstimateRowsWithoutRowGoal > node.EstimateRows)
{
var reduction = node.EstimateRowsWithoutRowGoal / node.EstimateRows;
Expand Down
32 changes: 26 additions & 6 deletions src/PlanViewer.Core/Services/ShowPlanParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -712,16 +712,36 @@ private static PlanNode ParseRelOp(XElement relOpEl)
var seekParts = new List<string>();
foreach (var sp in seekPreds)
{
var scalarOps = sp.Descendants(Ns + "ScalarOperator");
foreach (var so in scalarOps)
foreach (var seekKeys in sp.Elements(Ns + "SeekKeys"))
{
var val = so.Attribute("ScalarString")?.Value;
if (!string.IsNullOrEmpty(val))
seekParts.Add(val);
// Each SeekKeys has Prefix, StartRange, EndRange with ScanType
foreach (var range in seekKeys.Elements())
{
var scanType = range.Attribute("ScanType")?.Value;
var cols = range.Element(Ns + "RangeColumns")?
.Elements(Ns + "ColumnReference")
.Select(FormatColumnRef)
.ToList();
var exprs = range.Element(Ns + "RangeExpressions")?
.Elements(Ns + "ScalarOperator")
.Select(so => so.Attribute("ScalarString")?.Value ?? "?")
.ToList();

if (cols != null && exprs != null)
{
var op = scanType switch
{
"EQ" => "=", "GT" => ">", "GE" => ">=",
"LT" => "<", "LE" => "<=", _ => scanType ?? "="
};
for (int ci = 0; ci < cols.Count && ci < exprs.Count; ci++)
seekParts.Add($"{cols[ci]} {op} {exprs[ci]}");
}
}
}
}
if (seekParts.Count > 0)
node.SeekPredicates = string.Join(" AND ", seekParts);
node.SeekPredicates = string.Join(", ", seekParts);

// GuessedSelectivity — check if optimizer guessed selectivity on predicates
if (ScopedDescendants(physicalOpEl, Ns + "GuessedSelectivity").Any())
Expand Down
Loading