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
27 changes: 24 additions & 3 deletions src/PlanViewer.App/Controls/PlanViewerControl.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2187,7 +2187,7 @@ private void ShowParameters(PlanStatement statement)

if (parameters.Count == 0)
{
var localVars = FindUnresolvedVariables(statement.StatementText, parameters);
var localVars = FindUnresolvedVariables(statement.StatementText, parameters, statement.RootNode);
if (localVars.Count > 0)
{
ParametersHeader.Text = "Parameters";
Expand Down Expand Up @@ -2303,7 +2303,7 @@ private void ShowParameters(PlanStatement statement)
}
}

var unresolved = FindUnresolvedVariables(statement.StatementText, parameters);
var unresolved = FindUnresolvedVariables(statement.StatementText, parameters, statement.RootNode);
if (unresolved.Count > 0)
{
AddParameterAnnotation(
Expand Down Expand Up @@ -2350,7 +2350,8 @@ private void AddParameterAnnotation(string text, string color)
});
}

private static List<string> FindUnresolvedVariables(string queryText, List<PlanParameter> parameters)
private static List<string> FindUnresolvedVariables(string queryText, List<PlanParameter> parameters,
PlanNode? rootNode = null)
{
var unresolved = new List<string>();
if (string.IsNullOrEmpty(queryText))
Expand All @@ -2359,6 +2360,11 @@ private static List<string> FindUnresolvedVariables(string queryText, List<PlanP
var extractedNames = new HashSet<string>(
parameters.Select(p => p.Name), StringComparer.OrdinalIgnoreCase);

// Collect table variable names from the plan tree so we don't misreport them as local variables
var tableVarNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
if (rootNode != null)
CollectTableVariableNames(rootNode, tableVarNames);

var matches = Regex.Matches(queryText, @"@\w+", RegexOptions.IgnoreCase);
var seenVars = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

Expand All @@ -2369,6 +2375,8 @@ private static List<string> FindUnresolvedVariables(string queryText, List<PlanP
continue;
if (varName.StartsWith("@@", StringComparison.OrdinalIgnoreCase))
continue;
if (tableVarNames.Contains(varName))
continue;

seenVars.Add(varName);
unresolved.Add(varName);
Expand All @@ -2377,6 +2385,19 @@ private static List<string> FindUnresolvedVariables(string queryText, List<PlanP
return unresolved;
}

private static void CollectTableVariableNames(PlanNode node, HashSet<string> names)
{
if (!string.IsNullOrEmpty(node.ObjectName) && node.ObjectName.StartsWith("@"))
{
// ObjectName is like "@t.c" — extract the table variable name "@t"
var dotIdx = node.ObjectName.IndexOf('.');
var tvName = dotIdx > 0 ? node.ObjectName[..dotIdx] : node.ObjectName;
names.Add(tvName);
}
foreach (var child in node.Children)
CollectTableVariableNames(child, names);
}

private static void CollectWarnings(PlanNode node, List<PlanWarning> warnings)
{
warnings.AddRange(node.Warnings);
Expand Down
12 changes: 9 additions & 3 deletions src/PlanViewer.Core/Services/PlanAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -388,7 +388,7 @@
var modifiesTableVar = false;
CheckForTableVariables(stmt.RootNode, isModification, ref hasTableVar, ref modifiesTableVar);

if (hasTableVar)
if (hasTableVar && !modifiesTableVar)
{
stmt.PlanWarnings.Add(new PlanWarning
{
Expand Down Expand Up @@ -865,11 +865,17 @@
if (!cfg.IsRuleDisabled(22) && !string.IsNullOrEmpty(node.ObjectName) &&
node.ObjectName.StartsWith("@"))
{
var isModificationOp = node.PhysicalOp.Contains("Insert", StringComparison.OrdinalIgnoreCase)
|| node.PhysicalOp.Contains("Update", StringComparison.OrdinalIgnoreCase)
|| node.PhysicalOp.Contains("Delete", StringComparison.OrdinalIgnoreCase);

node.Warnings.Add(new PlanWarning
{
WarningType = "Table Variable",
Message = "Table variable detected. Table variables lack column-level statistics, which causes bad row estimates, join choices, and memory grant decisions. Replace with a #temp table.",
Severity = PlanWarningSeverity.Warning
Message = isModificationOp
? "Modifying a table variable forces the entire plan to run single-threaded. Replace with a #temp table to allow parallel execution."
: "Table variable detected. Table variables lack column-level statistics, which causes bad row estimates, join choices, and memory grant decisions. Replace with a #temp table.",
Severity = isModificationOp ? PlanWarningSeverity.Critical : PlanWarningSeverity.Warning
});
}

Expand Down Expand Up @@ -943,7 +949,7 @@
// Rule 28: Row Count Spool — NOT IN with nullable column
// Pattern: Row Count Spool with high rewinds, child scan has IS NULL predicate,
// and statement text contains NOT IN
if (!cfg.IsRuleDisabled(28) && node.PhysicalOp.Contains("Row Count Spool"))

Check warning on line 952 in src/PlanViewer.Core/Services/PlanAnalyzer.cs

View workflow job for this annotation

GitHub Actions / build-and-test

Dereference of a possibly null reference.

Check warning on line 952 in src/PlanViewer.Core/Services/PlanAnalyzer.cs

View workflow job for this annotation

GitHub Actions / build-and-test

Dereference of a possibly null reference.
{
var rewinds = node.HasActualStats ? (double)node.ActualRewinds : node.EstimateRewinds;
if (rewinds > 10000 && HasNotInPattern(node, stmt))
Expand Down
Loading