From 8a62e61ff6b544b47cdd970c7071c55deccb7278 Mon Sep 17 00:00:00 2001 From: Erik Darling <2136037+erikdarlingdata@users.noreply.github.com> Date: Mon, 6 Apr 2026 09:37:55 -0400 Subject: [PATCH] Show nonclustered index count on modification operators (#167) Display a badge on INSERT/UPDATE/DELETE operators showing how many nonclustered indexes are maintained. The clustered index or heap is implicit; the NC count surfaces the hidden maintenance overhead. - Parse Object elements with IndexKind="NonClustered" inside Update/SimpleUpdate/CreateIndex operator elements - Show "+N NC" badge on the operator node - List index names in tooltip and properties panel - Only counts on modification operators, not read operators - 4 unit tests with INSERT, UPDATE, DELETE, and read-only plans Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Controls/PlanViewerControl.axaml.cs | 33 ++++++++++- src/PlanViewer.Core/Models/PlanModels.cs | 4 ++ .../Services/ShowPlanParser.cs | 16 ++++++ .../NonClusteredIndexCountTests.cs | 54 ++++++++++++++++++ .../Plans/multi_index_delete_plan.sqlplan | Bin 0 -> 9768 bytes .../Plans/multi_index_insert_plan.sqlplan | Bin 0 -> 41956 bytes .../Plans/multi_index_update_plan.sqlplan | Bin 0 -> 56612 bytes 7 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 tests/PlanViewer.Core.Tests/NonClusteredIndexCountTests.cs create mode 100644 tests/PlanViewer.Core.Tests/Plans/multi_index_delete_plan.sqlplan create mode 100644 tests/PlanViewer.Core.Tests/Plans/multi_index_insert_plan.sqlplan create mode 100644 tests/PlanViewer.Core.Tests/Plans/multi_index_update_plan.sqlplan diff --git a/src/PlanViewer.App/Controls/PlanViewerControl.axaml.cs b/src/PlanViewer.App/Controls/PlanViewerControl.axaml.cs index a475b2d..debb21c 100644 --- a/src/PlanViewer.App/Controls/PlanViewerControl.axaml.cs +++ b/src/PlanViewer.App/Controls/PlanViewerControl.axaml.cs @@ -485,6 +485,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), + Margin = new Thickness(4, 0, 0, 0), + VerticalAlignment = VerticalAlignment.Center, + Child = new TextBlock + { + Text = $"+{node.NonClusteredIndexCount} NC", + FontSize = 10, + FontWeight = FontWeight.SemiBold, + Foreground = Brushes.White + } + }; + iconRow.Children.Add(ncBadge); + } + stack.Children.Add(iconRow); // Operator name @@ -961,7 +982,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); @@ -1010,6 +1031,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)) @@ -2025,6 +2052,10 @@ private object BuildNodeTooltipContent(PlanNode node, List? allWarn 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/src/PlanViewer.Core/Models/PlanModels.cs b/src/PlanViewer.Core/Models/PlanModels.cs index 0f15fbd..ec0201b 100644 --- a/src/PlanViewer.Core/Models/PlanModels.cs +++ b/src/PlanViewer.Core/Models/PlanModels.cs @@ -338,6 +338,10 @@ public class PlanNode public int NoMatchingIndexCount { get; set; } public int PartialMatchingIndexCount { get; set; } + // Modification operator: nonclustered indexes maintained + public int NonClusteredIndexCount { get; set; } + public List NonClusteredIndexNames { get; set; } = new(); + // ConstantScan Values (parsed rows as displayable string) public string? ConstantScanValues { get; set; } diff --git a/src/PlanViewer.Core/Services/ShowPlanParser.cs b/src/PlanViewer.Core/Services/ShowPlanParser.cs index 637df45..5cf9f11 100644 --- a/src/PlanViewer.Core/Services/ShowPlanParser.cs +++ b/src/PlanViewer.Core/Services/ShowPlanParser.cs @@ -691,6 +691,22 @@ private static PlanNode ParseRelOp(XElement relOpEl) node.TableReferenceId = (int)ParseDouble(objEl.Attribute("TableReferenceId")?.Value); } + // Nonclustered indexes maintained by modification operators (Update/SimpleUpdate) + 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); + } + } + // Hash keys for hash match operators var hashKeysProbeEl = physicalOpEl.Element(Ns + "HashKeysProbe"); if (hashKeysProbeEl != null) diff --git a/tests/PlanViewer.Core.Tests/NonClusteredIndexCountTests.cs b/tests/PlanViewer.Core.Tests/NonClusteredIndexCountTests.cs new file mode 100644 index 0000000..5df2bd3 --- /dev/null +++ b/tests/PlanViewer.Core.Tests/NonClusteredIndexCountTests.cs @@ -0,0 +1,54 @@ +using PlanViewer.Core.Models; + +namespace PlanViewer.Core.Tests; + +public class NonClusteredIndexCountTests +{ + [Fact] + public void Update_WithFiveNonClusteredIndexes_CountIsFive() + { + var plan = PlanTestHelper.LoadAndAnalyze("multi_index_update_plan.sqlplan"); + var stmt = PlanTestHelper.FirstStatement(plan); + var updateNode = PlanTestHelper.FindNode(stmt.RootNode!, 1)!; + + Assert.Contains("Update", updateNode.PhysicalOp, StringComparison.OrdinalIgnoreCase); + Assert.Equal(5, updateNode.NonClusteredIndexCount); + } + + [Fact] + public void Insert_WithFiveNonClusteredIndexes_CountIsFive() + { + var plan = PlanTestHelper.LoadAndAnalyze("multi_index_insert_plan.sqlplan"); + var stmt = PlanTestHelper.FirstStatement(plan); + var insertNode = PlanTestHelper.FindNode(stmt.RootNode!, 0)!; + + Assert.Contains("Insert", insertNode.PhysicalOp, StringComparison.OrdinalIgnoreCase); + Assert.Equal(5, insertNode.NonClusteredIndexCount); + } + + [Fact] + public void Delete_WithFiveNonClusteredIndexes_CountIsFive() + { + var plan = PlanTestHelper.LoadAndAnalyze("multi_index_delete_plan.sqlplan"); + var stmt = PlanTestHelper.FirstStatement(plan); + var deleteNode = PlanTestHelper.FindNode(stmt.RootNode!, 0)!; + + Assert.Contains("Delete", deleteNode.PhysicalOp, StringComparison.OrdinalIgnoreCase); + Assert.Equal(5, deleteNode.NonClusteredIndexCount); + } + + [Fact] + public void ReadOperator_HasZeroNonClusteredIndexCount() + { + var plan = PlanTestHelper.LoadAndAnalyze("key_lookup_plan.sqlplan"); + var stmt = PlanTestHelper.FirstStatement(plan); + + void AssertZero(PlanNode node) + { + Assert.Equal(0, node.NonClusteredIndexCount); + foreach (var child in node.Children) + AssertZero(child); + } + AssertZero(stmt.RootNode!); + } +} diff --git a/tests/PlanViewer.Core.Tests/Plans/multi_index_delete_plan.sqlplan b/tests/PlanViewer.Core.Tests/Plans/multi_index_delete_plan.sqlplan new file mode 100644 index 0000000000000000000000000000000000000000..744493746f6064270aa2835ff0bc72a886714983 GIT binary patch literal 9768 zcmeI2TTdHD7>4J%QvZXMt5#|_g+PdCQe%_E3IrES(h8vnY?IJpLu?ZuKfdkr&gWrg z4?aL>+6!cvU3R|nd1n3hU#sr9yL26Q;!5tbYrCH7yJL6iy6$K9z|FfEH^;TaO4l`c z-eR@uKKRvhH*iPpAy7-eJa^CB_wGBl>UP{mATL}Uss}*-<~H31G>lId4o|u7LrXk9 zLS^92cs9%D!hdG?`v~5r$n$}x3rH~spV=X;9c1~4OdaS;x-8}6JE8p0>)J;;Y0-qA zc)cyO(qn9}=-1cWId~^S?PMF-aE8?d|C!-$TIReJ`jLKpa63?JVvAF(c8bjhJd-y% zSfuYh!L9G5G7koOEwVc2mOYGlZoMwUT@1UIDc|gp|eh;rq)NK37<+?PZJkNUC3bJoT{6D{RsyZ%u_(PfUiMedeZOJj#>lBH6`2J?JZpf`_2Do`nV+blWBA}ZU+Ho%U@ zT;s>@dG``7trkyZysQ7QN> zDEz^Fllzccqaxby4zRnZE8>pb7cYlpN`oi~aTGFLl+Qh+@~e*5lxt1y%fPB~$g9>% zj-X|=Fs^W+LWOHlr80dj_f3{QHY&R}#6iuy=ekSwZ17(;P7;_p@KruFpkI!Gdk5S$ zFa}pdQMIN)zLbfgEnpitY89TpA#%#BCDOfjzk{hfuOeykJS(%b*#u5}D!^?+Z5Mjg z49D1>%H&<(R6nZV)cCs&PJ_D_p6hl_%GnY1-+Dk&8A5I=D%*5?t9V;4#L3`;O%In8de-6;5k z>u8uwaTuz0e#6Rd6ny`rB+GmJU{i%de}u-tsG2u zRLNAPh99u5>GII~Gqz5mqxoi@>ZnuM*6q`DoVD36<$W%>xV z$TWM8kFaI>+6sCsphuW9Z{vf|)B4`Orgiaz7HXw=GvF!vk$jVVJBw8{r>c&H3=6iH-n*;ObQWr2LPb^c410u?#3t)L z_s-B&`IujyEWhg8iPu40_A_?1**&h^E8bVxoJ(r+61gmks`lpdx(0QV*k~6mTshx&&4ei+rPfbeR4ni|JS_Np|P50rm%IC_c?nqM+)c4yNfz zblw1^`k9`*vRzY{>0VxE4!=U@Ed2`9&wUIl%H`dX&vfy)avCxlztck& zM|zsNdUmKAossfdj*nhXYhgJX^GWAUu$gJ{$mdNAoy~nLAiXb6>Vr zO`DVPJ~Y3(71eJwlc=^>y)>@lCXod8HP#ZIaPn1H)k2+FHnO=*aG__~bL$Axc-cJt z|FzfwU47r@X|a9O^}6>~q66GpDVgxIwor!bku3}NR*LpPqpTG6WA_=S8E%x77IQ0c z-Tsz!Nhg+VcGo&J(VQN3vTgPR+O162H?0!{TCc&>u3&mx!#5eUo7Ns7OlR?F?m9co zNhf5YmHn*J<20}8d`5G&b|)SFzp0FY?rMsPCXge(vY@@;`)?vzWzVe>a-HvNk_{VND|{R72M;@NKhXvB?rNXAIrrnO;xLg{KI#rnv<65RPQT)4 zmy84NIn8WWe#b6<+Fqh|eV)`rLYsJ#H&3^-VyWoqgH)nf&$jbd7c# z|3$eOugjj{jpS|eBUmh4l`nU%o2dRf9uehqEZ)uwcQ2tRCY9$ds)Z=VJ!X1MO*`w9 zH|uUO=Iwi|Q7PUBkJmz&$^kq3n*n<=j`@|uElRHgrm3d%Jd(Y;`R1JzYI_XFA)#w3GusRJGaoetE?~ye^Skm{qsB~ yTUB`ZH=D&?u;|GD5+)BFAk`U1|Gq|0$$4NxCH-8}o}Ihc%wurIq3D-e&)h#@E|p{e literal 0 HcmV?d00001 diff --git a/tests/PlanViewer.Core.Tests/Plans/multi_index_insert_plan.sqlplan b/tests/PlanViewer.Core.Tests/Plans/multi_index_insert_plan.sqlplan new file mode 100644 index 0000000000000000000000000000000000000000..647304746e375c425e88ac9eec2fabedfbcfd82f GIT binary patch literal 41956 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 P>dhCdf5DpTZNT|IufEAA literal 0 HcmV?d00001 diff --git a/tests/PlanViewer.Core.Tests/Plans/multi_index_update_plan.sqlplan b/tests/PlanViewer.Core.Tests/Plans/multi_index_update_plan.sqlplan new file mode 100644 index 0000000000000000000000000000000000000000..0201133a3968d2b85de5b26bc18e53bb2bd13dd0 GIT binary patch literal 56612 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 literal 0 HcmV?d00001