From ed64dbcdf8aaa85f8b3a57b956b74a78231a1c49 Mon Sep 17 00:00:00 2001 From: Erik Darling <2136037+erikdarlingdata@users.noreply.github.com> Date: Wed, 29 Apr 2026 19:32:11 -0400 Subject: [PATCH] Rule 38: suppress when query has explicit MAXDOP 2 hint When the user explicitly sets OPTION (MAXDOP 2) in the query, the DOP cap is intentional and not the SQL Server Standard Edition batch-mode limitation, so the warning would be misleading. Suppress in that case (both the Standard-Edition-confirmed Warning path and the unknown-edition Info path). Mirrors the existing Rule 3 (Serial Plan) handling for MAXDOP 1 hints. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/PlanViewer.Core/Services/PlanAnalyzer.cs | 42 +++++++++++-------- .../PlanAnalyzerTests.cs | 33 +++++++++++++++ 2 files changed, 58 insertions(+), 17 deletions(-) diff --git a/src/PlanViewer.Core/Services/PlanAnalyzer.cs b/src/PlanViewer.Core/Services/PlanAnalyzer.cs index 7130a65..882ba6e 100644 --- a/src/PlanViewer.Core/Services/PlanAnalyzer.cs +++ b/src/PlanViewer.Core/Services/PlanAnalyzer.cs @@ -482,31 +482,39 @@ private static void AnalyzeStatement(PlanStatement stmt, AnalyzerConfig cfg, Ser if (!cfg.IsRuleDisabled(38) && stmt.DegreeOfParallelism == 2 && stmt.RootNode != null && HasBatchModeNode(stmt.RootNode)) { - var editionKnown = !string.IsNullOrEmpty(serverMetadata?.Edition); - if (editionKnown - && serverMetadata!.Edition!.Contains("Standard", StringComparison.OrdinalIgnoreCase)) + // Suppress when the user explicitly set MAXDOP 2 as a query hint — the DOP + // cap is intentional, not the Standard Edition batch-mode limitation. + var hasMaxdop2Hint = !string.IsNullOrEmpty(stmt.StatementText) + && Regex.IsMatch(stmt.StatementText, @"MAXDOP\s+2\b", RegexOptions.IgnoreCase); + + if (!hasMaxdop2Hint) { - // Server context confirms Standard Edition — check MAXDOP - if (serverMetadata.MaxDop > 2) + var editionKnown = !string.IsNullOrEmpty(serverMetadata?.Edition); + if (editionKnown + && serverMetadata!.Edition!.Contains("Standard", StringComparison.OrdinalIgnoreCase)) + { + // Server context confirms Standard Edition — check MAXDOP + if (serverMetadata.MaxDop > 2) + { + stmt.PlanWarnings.Add(new PlanWarning + { + WarningType = "Standard Edition DOP Limitation", + Message = $"DOP is limited to 2 because SQL Server Standard Edition caps parallelism at 2 when batch mode operators are present, even though MAXDOP is set to {serverMetadata.MaxDop}. Developer or Enterprise Edition would allow higher DOP in the same conditions.", + Severity = PlanWarningSeverity.Warning + }); + } + } + else if (!editionKnown) { + // No server context, or edition unknown (e.g. collection failure) — suspect the limitation stmt.PlanWarnings.Add(new PlanWarning { WarningType = "Standard Edition DOP Limitation", - Message = $"DOP is limited to 2 because SQL Server Standard Edition caps parallelism at 2 when batch mode operators are present, even though MAXDOP is set to {serverMetadata.MaxDop}. Developer or Enterprise Edition would allow higher DOP in the same conditions.", - Severity = PlanWarningSeverity.Warning + Message = "DOP is limited to 2 and the plan uses batch mode operators. This may be caused by the SQL Server Standard Edition limitation, which caps parallelism at 2 when batch mode is in use. If this server runs Standard Edition, Developer or Enterprise Edition would allow higher DOP.", + Severity = PlanWarningSeverity.Info }); } } - else if (!editionKnown) - { - // No server context, or edition unknown (e.g. collection failure) — suspect the limitation - stmt.PlanWarnings.Add(new PlanWarning - { - WarningType = "Standard Edition DOP Limitation", - Message = "DOP is limited to 2 and the plan uses batch mode operators. This may be caused by the SQL Server Standard Edition limitation, which caps parallelism at 2 when batch mode is in use. If this server runs Standard Edition, Developer or Enterprise Edition would allow higher DOP.", - Severity = PlanWarningSeverity.Info - }); - } } // Rules 25 (Ineffective Parallelism) and 31 (Parallel Wait Bottleneck) were removed. diff --git a/tests/PlanViewer.Core.Tests/PlanAnalyzerTests.cs b/tests/PlanViewer.Core.Tests/PlanAnalyzerTests.cs index 04678d2..a49eb32 100644 --- a/tests/PlanViewer.Core.Tests/PlanAnalyzerTests.cs +++ b/tests/PlanViewer.Core.Tests/PlanAnalyzerTests.cs @@ -989,5 +989,38 @@ public void Rule38_ServerMetadataWithNullEdition_Dop2_BatchMode_EmitsInfo() Assert.Equal(PlanWarningSeverity.Info, warnings[0].Severity); } + [Fact] + public void Rule38_StandardEdition_Dop2_BatchMode_MaxDop2QueryHint_NoWarning() + { + // User explicitly set OPTION (MAXDOP 2) — DOP cap is intentional, not the SE limit + var stmt = BuildBatchModeDop2Statement(); + stmt.StatementText = "SELECT * FROM dbo.Fact OPTION (MAXDOP 2)"; + var plan = BuildSyntheticPlan(stmt); + var metadata = new ServerMetadata + { + Edition = "Standard Edition (64-bit)", + MaxDop = 8 + }; + PlanAnalyzer.Analyze(plan, serverMetadata: metadata); + + var warnings = stmt.PlanWarnings + .Where(w => w.WarningType == "Standard Edition DOP Limitation").ToList(); + Assert.Empty(warnings); + } + + [Fact] + public void Rule38_NoServerMetadata_Dop2_BatchMode_MaxDop2QueryHint_NoWarning() + { + // Same suppression applies when we don't know the edition either + var stmt = BuildBatchModeDop2Statement(); + stmt.StatementText = "SELECT * FROM dbo.Fact OPTION (MAXDOP 2)"; + var plan = BuildSyntheticPlan(stmt); + PlanAnalyzer.Analyze(plan); + + var warnings = stmt.PlanWarnings + .Where(w => w.WarningType == "Standard Edition DOP Limitation").ToList(); + Assert.Empty(warnings); + } + #endregion }