diff --git a/Dashboard/Controls/PlanViewerControl.xaml.cs b/Dashboard/Controls/PlanViewerControl.xaml.cs index 3ec40d49..247581d3 100644 --- a/Dashboard/Controls/PlanViewerControl.xaml.cs +++ b/Dashboard/Controls/PlanViewerControl.xaml.cs @@ -279,6 +279,27 @@ private Border CreateNodeVisual(PlanNode node, int totalWarningCount = -1) iconRow.Children.Add(parBadge); } + // Nonclustered index count badge (modification operators maintaining multiple NC indexes) + if (node.NonClusteredIndexCount > 0) + { + var ncBadge = new Border + { + Background = new SolidColorBrush(Color.FromRgb(0x6C, 0x75, 0x7D)), + CornerRadius = new CornerRadius(4), + Padding = new Thickness(4, 1, 4, 1), + Margin = new Thickness(4, 0, 0, 0), + VerticalAlignment = VerticalAlignment.Center, + Child = new TextBlock + { + Text = $"+{node.NonClusteredIndexCount} NC", + FontSize = 10, + FontWeight = FontWeights.SemiBold, + Foreground = Brushes.White + } + }; + iconRow.Children.Add(ncBadge); + } + stack.Children.Add(iconRow); // Operator name — use full name, let TextTrimming handle overflow @@ -693,7 +714,7 @@ private void ShowPropertiesPanel(PlanNode node) || node.SortDistinct || node.StartupExpression || node.NLOptimized || node.WithOrderedPrefetch || node.WithUnorderedPrefetch || node.WithTies || node.Remoting || node.LocalParallelism - || node.SpoolStack || node.DMLRequestSort + || node.SpoolStack || node.DMLRequestSort || node.NonClusteredIndexCount > 0 || !string.IsNullOrEmpty(node.OffsetExpression) || node.TopRows > 0 || !string.IsNullOrEmpty(node.ConstantScanValues) || !string.IsNullOrEmpty(node.UdxUsedColumns); @@ -742,6 +763,12 @@ private void ShowPropertiesPanel(PlanNode node) AddPropertyRow("Primary Node Id", $"{node.PrimaryNodeId}"); if (node.DMLRequestSort) AddPropertyRow("DML Request Sort", "True"); + if (node.NonClusteredIndexCount > 0) + { + AddPropertyRow("NC Indexes Maintained", $"{node.NonClusteredIndexCount}"); + foreach (var ixName in node.NonClusteredIndexNames) + AddPropertyRow("", ixName, isCode: true); + } if (!string.IsNullOrEmpty(node.ActionColumn)) AddPropertyRow("Action Column", node.ActionColumn, isCode: true); if (!string.IsNullOrEmpty(node.SegmentColumn)) @@ -1652,6 +1679,10 @@ private ToolTip BuildNodeTooltip(PlanNode node, List? allWarnings = AddTooltipRow(stack, "Scan Direction", node.ScanDirection); } + // NC index maintenance count + if (node.NonClusteredIndexCount > 0) + AddTooltipRow(stack, "NC Indexes Maintained", string.Join(", ", node.NonClusteredIndexNames)); + // Operator details (key items only in tooltip) var hasTooltipDetails = !string.IsNullOrEmpty(node.OrderBy) || !string.IsNullOrEmpty(node.TopExpression) diff --git a/Dashboard/Models/PlanModels.cs b/Dashboard/Models/PlanModels.cs index 57d6a25e..d3c75277 100644 --- a/Dashboard/Models/PlanModels.cs +++ b/Dashboard/Models/PlanModels.cs @@ -251,6 +251,10 @@ public class PlanNode public List Warnings { get; set; } = new(); public bool HasWarnings => Warnings.Count > 0; + // Modification operator: nonclustered indexes maintained + public int NonClusteredIndexCount { get; set; } + public List NonClusteredIndexNames { get; set; } = new(); + // Tree structure public List Children { get; set; } = new(); public PlanNode? Parent { get; set; } diff --git a/Dashboard/Services/ShowPlanParser.cs b/Dashboard/Services/ShowPlanParser.cs index 00668790..b8c45c64 100644 --- a/Dashboard/Services/ShowPlanParser.cs +++ b/Dashboard/Services/ShowPlanParser.cs @@ -972,6 +972,22 @@ private static PlanNode ParseRelOp(XElement relOpEl) if (actionColEl != null) node.ActionColumn = FormatColumnRef(actionColEl); + // Nonclustered indexes maintained by modification operators (Update/SimpleUpdate/CreateIndex) + var opName = physicalOpEl.Name.LocalName; + if (opName is "Update" or "SimpleUpdate" or "CreateIndex") + { + var ncObjects = ScopedDescendants(physicalOpEl, Ns + "Object") + .Where(o => string.Equals(o.Attribute("IndexKind")?.Value, "NonClustered", StringComparison.OrdinalIgnoreCase)) + .ToList(); + node.NonClusteredIndexCount = ncObjects.Count; + foreach (var ncObj in ncObjects) + { + var ixName = ncObj.Attribute("Index")?.Value?.Replace("[", "").Replace("]", ""); + if (!string.IsNullOrEmpty(ixName)) + node.NonClusteredIndexNames.Add(ixName); + } + } + // SET predicate (UPDATE operator) var setPredicateEl = physicalOpEl.Element(Ns + "SetPredicate"); if (setPredicateEl != null) diff --git a/Lite/Controls/PlanViewerControl.xaml.cs b/Lite/Controls/PlanViewerControl.xaml.cs index fc40d6a4..fb4af2e5 100644 --- a/Lite/Controls/PlanViewerControl.xaml.cs +++ b/Lite/Controls/PlanViewerControl.xaml.cs @@ -295,6 +295,27 @@ private Border CreateNodeVisual(PlanNode node, int totalWarningCount = -1) iconRow.Children.Add(parBadge); } + // Nonclustered index count badge (modification operators maintaining multiple NC indexes) + if (node.NonClusteredIndexCount > 0) + { + var ncBadge = new Border + { + Background = new SolidColorBrush(Color.FromRgb(0x6C, 0x75, 0x7D)), + CornerRadius = new CornerRadius(4), + Padding = new Thickness(4, 1, 4, 1), + Margin = new Thickness(4, 0, 0, 0), + VerticalAlignment = VerticalAlignment.Center, + Child = new TextBlock + { + Text = $"+{node.NonClusteredIndexCount} NC", + FontSize = 10, + FontWeight = FontWeights.SemiBold, + Foreground = Brushes.White + } + }; + iconRow.Children.Add(ncBadge); + } + stack.Children.Add(iconRow); // Operator name — use full name, let TextTrimming handle overflow @@ -710,7 +731,7 @@ private void ShowPropertiesPanel(PlanNode node) || node.SortDistinct || node.StartupExpression || node.NLOptimized || node.WithOrderedPrefetch || node.WithUnorderedPrefetch || node.WithTies || node.Remoting || node.LocalParallelism - || node.SpoolStack || node.DMLRequestSort + || node.SpoolStack || node.DMLRequestSort || node.NonClusteredIndexCount > 0 || !string.IsNullOrEmpty(node.OffsetExpression) || node.TopRows > 0 || !string.IsNullOrEmpty(node.ConstantScanValues) || !string.IsNullOrEmpty(node.UdxUsedColumns); @@ -759,6 +780,12 @@ private void ShowPropertiesPanel(PlanNode node) AddPropertyRow("Primary Node Id", $"{node.PrimaryNodeId}"); if (node.DMLRequestSort) AddPropertyRow("DML Request Sort", "True"); + if (node.NonClusteredIndexCount > 0) + { + AddPropertyRow("NC Indexes Maintained", $"{node.NonClusteredIndexCount}"); + foreach (var ixName in node.NonClusteredIndexNames) + AddPropertyRow("", ixName, isCode: true); + } if (!string.IsNullOrEmpty(node.ActionColumn)) AddPropertyRow("Action Column", node.ActionColumn, isCode: true); if (!string.IsNullOrEmpty(node.SegmentColumn)) @@ -1664,6 +1691,10 @@ private ToolTip BuildNodeTooltip(PlanNode node, List? allWarnings = AddTooltipRow(stack, "Scan Direction", node.ScanDirection); } + // NC index maintenance count + if (node.NonClusteredIndexCount > 0) + AddTooltipRow(stack, "NC Indexes Maintained", string.Join(", ", node.NonClusteredIndexNames)); + // Operator details (key items only in tooltip) var hasTooltipDetails = !string.IsNullOrEmpty(node.OrderBy) || !string.IsNullOrEmpty(node.TopExpression) diff --git a/Lite/Models/PlanModels.cs b/Lite/Models/PlanModels.cs index a6e62f77..50a33927 100644 --- a/Lite/Models/PlanModels.cs +++ b/Lite/Models/PlanModels.cs @@ -251,6 +251,10 @@ public class PlanNode public List Warnings { get; set; } = new(); public bool HasWarnings => Warnings.Count > 0; + // Modification operator: nonclustered indexes maintained + public int NonClusteredIndexCount { get; set; } + public List NonClusteredIndexNames { get; set; } = new(); + // Tree structure public List Children { get; set; } = new(); public PlanNode? Parent { get; set; } diff --git a/Lite/Services/ShowPlanParser.cs b/Lite/Services/ShowPlanParser.cs index b30fb0f1..bd08ff22 100644 --- a/Lite/Services/ShowPlanParser.cs +++ b/Lite/Services/ShowPlanParser.cs @@ -972,6 +972,22 @@ private static PlanNode ParseRelOp(XElement relOpEl) if (actionColEl != null) node.ActionColumn = FormatColumnRef(actionColEl); + // Nonclustered indexes maintained by modification operators (Update/SimpleUpdate/CreateIndex) + var opName = physicalOpEl.Name.LocalName; + if (opName is "Update" or "SimpleUpdate" or "CreateIndex") + { + var ncObjects = ScopedDescendants(physicalOpEl, Ns + "Object") + .Where(o => string.Equals(o.Attribute("IndexKind")?.Value, "NonClustered", StringComparison.OrdinalIgnoreCase)) + .ToList(); + node.NonClusteredIndexCount = ncObjects.Count; + foreach (var ncObj in ncObjects) + { + var ixName = ncObj.Attribute("Index")?.Value?.Replace("[", "").Replace("]", ""); + if (!string.IsNullOrEmpty(ixName)) + node.NonClusteredIndexNames.Add(ixName); + } + } + // SET predicate (UPDATE operator) var setPredicateEl = physicalOpEl.Element(Ns + "SetPredicate"); if (setPredicateEl != null)