Skip to content

Plan analysis: skip Row Estimate Mismatch on unexecuted operators, skip engine functions in the TVF rule (#4522, #4525) - #4542

Merged
erikdarlingdata merged 2 commits into
devfrom
plan-sync/4522-4525
Sep 28, 2026
Merged

erikdarlingdata merged 2 commits into
devfrom
plan-sync/4522-4525

Conversation

@erikdarlingdata

@erikdarlingdata erikdarlingdata commented Sep 28, 2026 •

Copy link
Copy Markdown
Owner

Fixes #4522
Fixes #4525
Part of #4511

Why

Two of the plan analyzer's rules fired when they shouldn't have.

Rule 5 (Row Estimate Mismatch) checked only that an operator had actual stats and a nonzero row estimate. An operator in a branch that never ran (one side of a concatenation, an outer join's unreached inner side) returns 0 rows because it never executed — that zero says nothing about whether the estimate was right. The rule warned anyway.

Rule 23 (Table-Valued Function) fired on every operator with LogicalOp == "Table-valued function", including the engine's own functions: STRING_SPLIT, OPENJSON, GENERATE_SERIES, and every DMV/DMF. Its advice (rewrite as an inline function, or stage rows in a #temp table) is for user-written multi-statement TVFs, which have no statistics. The engine's functions aren't user code and the advice doesn't apply to them.

What changes

  • PlanAnalyzer.cs rule 5: adds node.ActualExecutions > 0 to the condition, and removes the now-unreachable ActualExecutions > 0 ? ActualExecutions : 1 fallback (the divisor is always the real execution count once the new guard runs first).
  • PlanModels.cs: adds PlanNode.SchemaName, alongside the existing DatabaseName.
  • ShowPlanParser.cs: sets SchemaName from the parsed Object element, next to where DatabaseName is already set.
  • PlanAnalyzer.cs rule 23: skips an operator whose Object has neither a database nor a schema — how the engine's own functions appear in the plan XML. A user-written function always has both.

This mirrors erikdarlingdata/PerformanceStudio@cc18844 and erikdarlingdata/PerformanceStudio@de1cac6 (#4522) and erikdarlingdata/PerformanceStudio#584 (#4525), adapted only in that PerformanceMonitor has no AnalyzerConfig, so there's no cfg.IsRuleDisabled(...) guard to preserve.

Test plan

New tests: Darling.Tests.PlanSync4522Tests, Darling.Tests.PlanSync4525Tests. Both build a synthetic actual-plan XML (dbo.t, [db]) and run it through ShowPlanParser.Parse + PlanAnalyzer.Analyze — the product's own parse-then-analyze path, not a hand-built PlanNode.

  • PlanSync4522Tests.Rule05_OperatorThatNeverExecuted_IsNotAnEstimateMismatch: runtime RED on dev (Assert.False() Failure: Expected: False, Actual: True), and a still-warns mirror.
  • PlanSync4525Tests: the two rule-23 facts (an engine STRING_SPLIT → no warning; [db].[dbo] user function → still warns) use only existing members. The engine fact is a runtime RED on dev (Assert.DoesNotContain() Failure: Filter matched in collection, the TVF warning present). Parser_SetsSchemaName_NextToDatabaseName asserts the new PlanNode.SchemaName member, so it is a compile-only RED on dev.
  • Mutations, each reverted: dropping node.ActualExecutions > 0 from rule 5 fails the rule-5 fact; dropping !isEngineFunction from rule 23 fails the engine fact.
  • Run: the new classes plus the plan-analysis classes (ShowPlanParserCondAndMultiplePlanTests, the ActualPlan* classes, QueryModificationDetectorTests, ReproScriptBuilderHardeningTests, DarlingAnalysisPipelineTests, DarlingMcpPlanToolsSurfaceAndSqlTests, McpPlanAnalysisEnvelopeTests, SerialLoopStoreSizeSourceTests, TsqlConventionGuardTests, DocCommentHygieneTests): Total: 234, Failed: 0. The live-Postgres plan-tool class runs in CI. Lite.Tests builds; its plan classes run on Windows only.

CHANGELOG

SECTION: Fixed
ENTRY: - Plan analysis no longer flags an unexecuted operator as a row estimate mismatch, and stops recommending a rewrite for the engine's own table-valued functions (STRING_SPLIT, OPENJSON, GENERATE_SERIES, DMVs/DMFs) ([#4542]) - Rule 5 previously warned about a 0-row estimate mismatch on any operator whose branch never ran; it now requires ActualExecutions > 0. Rule 23 previously warned on every table-valued function operator; it now skips one whose object has neither a database nor a schema, which is how the engine's own functions appear.
REF: [#4542]: #4542

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant