Skip to content

Release v1.16.0 - Query Store search and filtering#394

Merged
erikdarlingdata merged 12 commits into
mainfrom
dev
Jul 21, 2026
Merged

Release v1.16.0 - Query Store search and filtering#394
erikdarlingdata merged 12 commits into
mainfrom
dev

Conversation

@erikdarlingdata

Copy link
Copy Markdown
Owner

Release v1.16.0

Query Store

Plan analysis

Tooling

Version bumped to 1.16.0 in Directory.Build.props and the SSMS extension (AssemblyInfo.cs + vsixmanifest) (#393).

Merging this PR triggers the release workflow: GitHub release with notes, signed Windows build, macOS/Linux builds, SSMS VSIX + gallery upload, and Velopack packages.

🤖 Generated with Claude Code

erikdarlingdata and others added 12 commits July 9, 2026 21:44
A self-contained Agent Skill that teaches any Claude agent to read a SQL Server
execution plan without needing PerformanceStudio installed. Nothing in it refers
to this repo, so the directory copies out cleanly.

The valuable content is negative knowledge -- the confident-sounding conclusions
that are wrong. Cost percentages are estimates even in actual plans. Row-mode
operator times are cumulative, so ranking by raw ActualElapsedms ranks by depth
and always crowns the root. Estimated vs actual rows must be normalized by
ActualExecutions or the inner side of every nested loop looks catastrophically
underestimated. Missing-index requests emit equality columns in arbitrary order
and ignore existing indexes.

Contents:
  SKILL.md         seven-step triage, ordered to establish ground truth before
                   forming an opinion, plus a "not evidence" list
  scripts/         extract.py flattens a .sqlplan into a digest; --node drills
                   into one operator; --sql recovers full statement text
  references/      timing, cardinality, warnings, parallelism, indexes, operators,
                   and a no-Python fallback

extract.py exists because .sqlplan files are UTF-16 (so grep silently matches
nothing), some declare an encoding they don't use, and a trivial plan is 120 KB
while the largest fixture here is 7 MB. It mirrors PlanAnalyzer.Timing.cs and
NodeTimeAttribution.cs for self-time attribution: batch mode reports standalone
times, row mode cumulative, exchange operators accumulate downstream wait time,
and parallel self-time must subtract within a thread rather than across threads.

Validated by seven agents with no prior context, run against the 299 example
plans. All reached correct conclusions, including on three traps: a plan with
nothing wrong, a no-join-predicate warning that was a false alarm, and an
estimated-only plan where the correct answer is to decline and ask for an actual
plan. Their findings fixed a real bug (Table Spool matched the Index Spool
detection, so update plans were told they needed an index), a documentation hole
that would have produced a wrong cross-join diagnosis, two factual errors about
scalar-UDF parallelism reasons and parallel self-time arithmetic, and an answer
key that had leaked from a test fixture into SKILL.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude Code never discovered this directory. Skills are only loaded from
~/.claude/skills/, .claude/skills/, or a plugin bundle -- a bare skills/ path at
a repo root does nothing. The copy added in #385 was therefore nine files of
documentation that no agent could load, and it had already drifted: its SKILL.md
still resolves scripts/extract.py against a repo path, which is wrong once the
skill is installed as a plugin and its directory is unguessable.

The skill now ships from erikdarlingdata/claude-plugins as a Claude Code plugin:

    /plugin marketplace add erikdarlingdata/claude-plugins
    /plugin install query-plan-analysis@erikdarling

It was moved out rather than promoted here because reaching this repo's default
branch requires a dev -> main PR, which auto-fires release.yml and demands a
version bump -- shipping a Velopack auto-update to every user for a change
containing no application code.

One coupling to keep in mind: the skill's references/timing.md is transcribed
from src/PlanViewer.Core/Services/PlanAnalyzer.Timing.cs and NodeTimeAttribution.cs
(row-mode cumulative vs batch-mode standalone times, exchange operators
accumulating downstream waits, per-thread rather than cross-thread subtraction).
If those semantics change, update the skill repo; nothing will catch the drift.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-copy

Remove inert skills/ copy of query-plan-analysis
…h subtrees

Two bugs in self-time attribution, both of which inflate an operator until it is
reported as the hottest in its plan. Found while building a portable query-plan
skill that mirrored this code, and confirmed against the fixture set.

1. THREAD 0 IS THE COORDINATOR.

In a parallel plan thread 0 carries no rows, and its ActualElapsedms is the wall
clock of the whole parallel branch. GetPerThreadOwnElapsed iterated every thread
including it, so any operator near the top of a parallel branch was handed the
branch's entire duration as its own work, and its self CPU underflowed to zero
because the coordinator burns none. ShowPlanParser also took the node-level
elapsed as the max across all threads, including the coordinator.

Measured across the .internal/examples corpus: of 1,014 non-exchange parallel
operators, the coordinator reports rows in 0 cases and elapsed in 65. Those 65
are precisely the operators that win a ranking by self time.

Serial plans have a single thread numbered 0, which IS a worker, so thread 0 is
only excluded when other threads exist.

2. THE PARALLEL PATH DID NOT LOOK THROUGH BATCH SUBTREES OR PASS-THROUGHS.

GetSerialOwnElapsed subtracts GetEffectiveChildElapsedMs, which looks through
Compute Scalar pass-throughs and sums contiguous batch zones. GetPerThreadOwnElapsed
subtracted each child's raw per-thread value instead, handling only Parallelism
children. So:

  - a batch-mode child reports STANDALONE time, so only its topmost value came
    off and the rest of the zone stayed inside the parent;
  - a pass-through child carries no runtime stats, so zero came off and the
    parent absorbed the entire subtree beneath it.

On cross-apply-point-in-time-slow.sqlplan a row-mode Nested Loops above a batch
Compute Scalar (0ms) hiding a batch Hash Aggregate (45,683ms) reported 45,737ms
of self time and led the ranking. Its real self time is 1ms, and the aggregate's
time was double-counted into both rows.

The same helpers now serve both paths, for elapsed and CPU. NodeTimeAttribution
(the display path) gets the matching batch-subtree summation so the graphical
plan and the analyzer agree.

TESTS

New OperatorSelfTimeTests, with a small synthetic fixture that encodes both traps
in one shape. Three of the six fail against the unfixed code, including
ParallelPassThroughChildDoesNotInflateItsParent, which uses the EXISTING
join_or_clause_plan fixture -- this was live on real plans.

PlanViewer.Core.csproj now grants InternalsVisibleTo the test project;
PlanAnalyzer.Timing is internal but is the source of every operator timing the UI
and BenefitScorer report, and it was untested.

Warning baseline: one line changes. join_or_clause_plan's Sort drops from
19,602ms to 18,580ms of self time -- the Compute Scalar subtree it had absorbed.
The warning still fires; the number is now correct. No warnings appear or
disappear on any other plan.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tor-and-batch

Fix operator self-time: exclude coordinator thread, look through batch subtrees
- QueryStoreFilter.QueryTextSearch + shared BuildQueryTextSearchPattern helper
  (sp_QuickieStore default semantics: trim, auto-wrap %, wildcards stay live)
- Parameterized EXISTS predicate applied pre-TOP-N in all three Query Store
  SQL builders (flat, grouped-by-hash, grouped-by-module)
- Desktop: Query Text option in the Search-by combo
- MCP: additive query_text_search param on get_query_store_top
- CLI: additive --query-text-search option
- Tests: helper pattern matrix + ParseExecutionType backfill (22 new cases)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…arch

Add Query Store query-text search filter
Core:
- QueryStoreFilter: MinExecutions, MinDurationMs, MinCpuMs (window-wide
  per-plan averages), QueryTextSearchNot, EscapeBrackets, and four
  include/ignore query-id/plan-id lists; strict ParseIdList (ordered
  dedupe, throws on garbage); BuildQueryTextSearchPattern escapeBrackets
  overload (escapes [ ] _ for ESCAPE '\', % stays live)
- All three SQL builders (flat, grouped-by-hash, grouped-by-module) gain
  the new clauses pre-TOP-N, fully parameterized; rn=1 relaxed when plans
  are explicitly included; unset path renders byte-identical SQL
- QueryStoreServerFilterState: UI-free, unit-testable filter model
  (typed setters, ordered chips, 1:1 BuildFilter projection)

Desktop:
- Collapsible Server Filters panel (thresholds, text include/exclude,
  escape toggle, id lists) with transactional Apply
- Chip strip showing every criterion that shaped the fetch; chips are
  removable and re-fetch
- Column-filter popups gain 'Search server' promotion on mapped columns
  (Executions/AvgCpu/AvgDuration/ids/hashes/text) with operator
  validation at click; totals deliberately not promotable
- Search-by commits become chips; Clear clears all server filters;
  panel expanded state persisted

MCP/CLI:
- Nine additive get_query_store_top params and query-store options

Tests: 59 new (escape-mode matrix, ParseIdList contract, filter-state
model) - 182 total passing

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add Query Store server-side filter panel, chips, and pre-filters
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@erikdarlingdata
erikdarlingdata merged commit 9b0d1e0 into main Jul 21, 2026
3 checks passed
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