From 2664ff551755a499c5f9c0edc880189829485a3d Mon Sep 17 00:00:00 2001 From: Erik Darling <2136037+erikdarlingdata@users.noreply.github.com> Date: Sun, 8 Mar 2026 08:18:16 -0500 Subject: [PATCH] Root tooltip: distinct warning names, seek predicate formatting, row goal filter - Root node tooltip shows distinct warning type names with counts instead of full messages - Seek predicates now show Column = Value format (e.g. Posts.PostTypeId = (2)) - Rule 26 (Row Goal) limited to data access operators only (seeks/scans) Co-Authored-By: Claude Opus 4.6 --- .../Controls/PlanViewerControl.axaml.cs | 48 +++++++++++++++---- src/PlanViewer.Core/Services/PlanAnalyzer.cs | 6 ++- .../Services/ShowPlanParser.cs | 32 ++++++++++--- 3 files changed, 69 insertions(+), 17 deletions(-) diff --git a/src/PlanViewer.App/Controls/PlanViewerControl.axaml.cs b/src/PlanViewer.App/Controls/PlanViewerControl.axaml.cs index 0c4b1a0..fffd89f 100644 --- a/src/PlanViewer.App/Controls/PlanViewerControl.axaml.cs +++ b/src/PlanViewer.App/Controls/PlanViewerControl.axaml.cs @@ -1863,18 +1863,46 @@ private object BuildNodeTooltipContent(PlanNode node, List? 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) + }); + } } } diff --git a/src/PlanViewer.Core/Services/PlanAnalyzer.cs b/src/PlanViewer.Core/Services/PlanAnalyzer.cs index 8f2a6c6..fbeae53 100644 --- a/src/PlanViewer.Core/Services/PlanAnalyzer.cs +++ b/src/PlanViewer.Core/Services/PlanAnalyzer.cs @@ -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; diff --git a/src/PlanViewer.Core/Services/ShowPlanParser.cs b/src/PlanViewer.Core/Services/ShowPlanParser.cs index 98a2c76..86e2e74 100644 --- a/src/PlanViewer.Core/Services/ShowPlanParser.cs +++ b/src/PlanViewer.Core/Services/ShowPlanParser.cs @@ -712,16 +712,36 @@ private static PlanNode ParseRelOp(XElement relOpEl) var seekParts = new List(); 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())