Add portable query-plan-analysis skill#385
Merged
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
A self-contained Agent Skill at
skills/query-plan-analysis/that teaches any Claude agent to read a SQL Server execution plan without PerformanceStudio installed. Nothing in it references this repo, so the directory copies out cleanly for anyone.Why this shape
The first instinct was to extract PlanViewer's 37
RuleNN_detection methods into a rule catalog. That was wrong. An LLM spotsLogicalOp == "Eager Spool"in XML unaided — detection is the disposable part. What a model can't do is know the things that make plan analysis sound authoritative and be wrong:ActualElapsedmsranks by depth and always crowns the root node.EstimateRowsis per-execution;ActualRowsis a total. Without dividing byActualExecutions, the inner side of every nested loop looks catastrophically underestimated.So the skill is built out of negative knowledge — the conclusions that are unsafe.
Contents
SKILL.mdscripts/extract.py.sqlplaninto a digest.--node Ndrills into one operator;--sqlrecovers full statement text. Stdlib only.references/timing,cardinality,warnings,parallelism,indexes,operators, plus a no-Python fallback.Why an extractor is mandatory, not a nicety
.sqlplanfiles are UTF-16.grep PlanAffectingConvert plan.sqlplanmatches nothing and reports no error, so a negative result is worthless.encoding="utf-16"(opened and re-saved). Strict XML parsers reject them.extract.pymirrorsPlanAnalyzer.Timing.csandNodeTimeAttribution.csfor self-time attribution rather than trusting anyone's memory of the semantics: batch mode reports standalone times, row mode cumulative, exchange operators accumulate downstream wait time and their thread 0 is a coordinator, and parallel self-time must subtract within a thread rather than across threads.Test plan
Seven agents with no prior context were given only the skill directory and a plan file, then graded against ground truth. All seven reached correct conclusions.
Three were deliberate traps:
HAVING COUNT_BIG(*) > POWER(2., 31) - 1can never fire on a 2.46M-row table.A fourth had decoys: a trivial 26-write sort spill and a missing-index request sitting next to a scalar UDF consuming 13,693 of the query's 13,694 seconds. The agent named the UDF and explicitly dismissed both decoys.
extract.pyruns clean on 298 of 299 example plans. The holdout isgarbage.sqlplan, which is deliberately invalid and now fails with a readable message instead of a stack trace.What testing found
Agent feedback fixed real defects, not cosmetics:
"Spool" in physical and "Eager" in logical, which also matches Table Spool (Eager Spool) — ordinary Halloween protection in any update plan. The tool told update plans they needed an index, whileindexes.mdnext to it said those spools are not defects.warnings.mdgave positive tells for two of three no-join-predicate cases. The missing one — both inputs pinned to the same constant, no outer references, no predicate — is the most common, and a reader applying the two stated tests literally would have diagnosed an accidental cross join.TSQLUserDefinedFunctionsNotParallelizableis not the only scalar-UDF serial-plan tell (CouldNotGenerateValidParallelPlanis at least as common), and the "do self times sum to the total?" sanity check is valid only for serial plans — in a parallel plan they legitimately exceed it, so the check as written would have talked an agent out of a correct answer.SKILL.md's worked example quoted the exact numbers of one of the test fixtures.Writing the extractor caught two more before any agent saw it: self-elapsed printed next to cumulative CPU, and row-mode child subtraction applied to batch-mode CPU, underflowing a 41-second hash aggregate to "0 ms self CPU." Both are exactly the failures the skill exists to prevent, and neither surfaced until it ran against real plans.
One suggestion was declined: having the digest auto-classify warnings as benign or genuine. It has the inputs, but a confident wrong verdict is worse than none and is the precise failure mode this skill targets. It prints the facts and says
INCONCLUSIVEwhen the row counts cannot discriminate.Note
references/timing.mdis transcribed fromPlanAnalyzer.Timing.cs. If that file's semantics change, the skill drifts.🤖 Generated with Claude Code