Skip to content

Fix QUERY_HIGH_DOP firing on a stale lifetime max_dop - #2706

Merged
erikdarlingdata merged 1 commit into
devfrom
fix/query-high-dop-stale-maxdop
Aug 31, 2026
Merged

erikdarlingdata merged 1 commit into
devfrom
fix/query-high-dop-stale-maxdop

Conversation

@erikdarlingdata

Copy link
Copy Markdown
Owner

Fixes #2705.

Problem

`PgFactCollector.CollectQueryStatsFactsAsync` counted `v_query_stats.max_dop > 8` unconditionally and fed the count into a `QUERY_HIGH_DOP` Fact at full confidence. `max_dop` is `sys.dm_exec_query_stats`' lifetime-max for the plan's time in cache — the exact semantics `PerformanceMonitor.Common/QueryStatExtremes.cs` (#2235) already documents for `min/max_cpu_ms`, and the same caveat `get_top_queries_by_cpu`'s tool description has always carried. A plan compiled before an operator lowered `max degree of parallelism` keeps reporting its old, higher DOP until evicted or recompiled — so on a server now configured to MAXDOP 1, a cached `max_dop = 16` is not a current problem, it's a stale high-water mark, and the finding fired anyway at severity 1.00.

Fix

Same "provable tell" doctrine `QueryStatExtremes` already applies to CPU/elapsed: a `max_dop` reading that exceeds the server's current `max degree of parallelism` (from `server_config`) is impossible under today's configuration, so it provably predates whatever change set that config, and is excluded from the count. A current value of 0 (unlimited) or no `server_config` row at all proves nothing impossible, so those cases are unchanged.

Test plan

  • `dotnet build` (Darling.Analysis + Darling.Tests) clean, 0 errors
  • Verified against a real Postgres 17 (a throwaway console harness against a local docker container, referencing the shipped project directly — not a retyped copy of the SQL): reverting the fix reproduces the exact reported false positive (current maxdop=1, stale `max_dop=16` still fires `QUERY_HIGH_DOP`); restoring the fix suppresses that case while the maxdop=0 (unlimited) and unknown-`server_config` cases still fire exactly as before.
  • Added `QueryHighDopStaleMaxDopLiveTests` (live-Postgres, `[Collection("live-postgres")]`, skips without `DARLING_TEST_PG`) covering all three cases — this is the CI-executed version of the same three scenarios the local harness proved.
  • Could not run `Darling.Tests` locally end-to-end (this Mac lacks the WindowsDesktop runtime the `net10.0-windows` TFM needs) — relying on the "Darling PostgreSQL tests" CI job, which runs against a live Postgres and is exactly the job this test is written for.

🤖 Generated with Claude Code

v_query_stats.max_dop is sys.dm_exec_query_stats' lifetime-max for a
plan's time in cache - the same semantics QueryStatExtremes.cs already
documents for min/max_cpu_ms, and the same caveat get_top_queries_by_cpu's
tool description has always carried for a human reader. QUERY_HIGH_DOP
had no equivalent guard: it counted max_dop > 8 unconditionally and fed
the raw lifetime value into a Fact at full confidence, so a plan compiled
before an operator lowered MAXDOP kept tripping the finding forever.

Apply the same provable-tell doctrine QueryStatExtremes uses for CPU and
elapsed extremes: a max_dop reading that exceeds the server's CURRENT
max degree of parallelism setting is impossible under today's config, so
it provably predates whatever change set that config and must not be
counted. A current value of 0 (unlimited) or no server_config row at all
proves nothing impossible, so the count is unchanged in both cases.

Verified against a live Postgres 17 container: reverting the fix
reproduces the exact reported false positive (maxdop=1, stale max_dop=16
still fires QUERY_HIGH_DOP); restoring it suppresses that case while
leaving the maxdop=0 and unknown-config cases firing exactly as before.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@claude

claude Bot commented Aug 31, 2026

Copy link
Copy Markdown

Review

The Darling-side fix itself looks correct: the current_maxdop CTE is scoped by server_id, the LEFT JOIN's NULL-safe fallback correctly leaves the count untouched when server_config has no row or reports 0 (unlimited), and the "exceeds current config -> provably stale" exclusion matches the doctrine already documented in PerformanceMonitor.Common/QueryStatExtremes.cs. SQL style (aliasing, AND alignment) matches the surrounding file. The new live test exercises all three branches (stale/excluded, unlimited, unknown-config) against a real Postgres engine, which is the right level of rigor for a JOIN-dependent fix like this.

Lite/Darling parity drift

This fixes only the Darling collector. Lite/Analysis/DuckDbFactCollector.QueryPerf.cs (line 31) has the exact same unconditional COUNT(CASE WHEN max_dop > 8 THEN 1 END) -- the identical false positive described in issue 2705 will still fire on Lite. Lite already collects server_config rows including 'max degree of parallelism' (see Lite/Analysis/DuckDbFactCollector.Config.cs), and QueryStatExtremes.cs (the doctrine this fix cites) is shared PerformanceMonitor.Common code consumed by both apps' MCP tools -- so the same guard is applicable to Lite with no missing infrastructure. Since this repo requires the two apps to stay in parity, the DuckDB query should get the equivalent current_maxdop CTE/JOIN treatment (translated to DuckDB SQL) in the same PR or a fast-follow, otherwise Lite and Darling will disagree on this finding for the same underlying data.

@erikdarlingdata
erikdarlingdata merged commit 957daaf into dev Aug 31, 2026
6 checks passed
@erikdarlingdata
erikdarlingdata deleted the fix/query-high-dop-stale-maxdop branch August 31, 2026 10:43
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