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
33 changes: 32 additions & 1 deletion src/PlanViewer.App/Controls/PlanViewerControl.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -2025,6 +2052,10 @@ private object BuildNodeTooltipContent(PlanNode node, List<PlanWarning>? 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)
Expand Down
4 changes: 4 additions & 0 deletions src/PlanViewer.Core/Models/PlanModels.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> NonClusteredIndexNames { get; set; } = new();

// ConstantScan Values (parsed rows as displayable string)
public string? ConstantScanValues { get; set; }

Expand Down
16 changes: 16 additions & 0 deletions src/PlanViewer.Core/Services/ShowPlanParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
54 changes: 54 additions & 0 deletions tests/PlanViewer.Core.Tests/NonClusteredIndexCountTests.cs
Original file line number Diff line number Diff line change
@@ -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!);
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading