Skip to content

sp_QuickieStore: @find_parameter_sensitive fixes from prod dogfooding - #854

Open
erikdarlingdata wants to merge 3 commits into
devfrom
fix-ps-mode-dogfood
Open

sp_QuickieStore: @find_parameter_sensitive fixes from prod dogfooding#854
erikdarlingdata wants to merge 3 commits into
devfrom
fix-ps-mode-dogfood

Conversation

@erikdarlingdata

Copy link
Copy Markdown
Owner

What

Five changes to @find_parameter_sensitive, all driven by dogfooding the mode against an 11-database production fleet (SQL Server 2019/2022 on RDS, Query Store catalogs with 4K–15K plan shapes per 72h window), plus a CI workflow that gives dev a fresh compiled Install-All without recreating the old merge-conflict problem.

1. Work-weighted ranking

Coefficient of variation alone is scale-blind: in the fleet run, sub-1s-max-CPU shapes held 91 of 162 top-15 slots because a 3ms→60ms swing out-CoVs a 2s→40s one. Selection and presentation now rank on volatility * LOG10(10 + total work in the window for the ranked metric). On quiet systems the weight is near-uniform (log of tiny totals ≈ 1) so behavior is effectively unchanged; on busy systems a shape must be both volatile and expensive. volatility_score still shows the raw CoV; ranked_on says work-weighted so output describes itself. The internal sort_value column stays internal (test asserts it doesn't leak).

Prod evidence (same params, same fleet, old vs. patched build side by side): window CPU represented by the surfaced top-15 rose from 295s → 4,170s on one tenant and 829s → 2,884s on another, with only 2–4 swaps per database — the swapped-in shapes included a 3,507 CPU-s parameter-sensitive adhoc (96K execs) and a 2,177 CPU-s shape (461K execs) that pure CoV had ranked below sub-second queries.

2. Hash-based include/ignore lists are honored

Shapes are keyed by query_hash + query_plan_hash, so @include_query_hashes / @ignore_query_hashes / @include_plan_hashes / @ignore_plan_hashes map directly. They're parsed locally in the mode block (the main filter section is never reached from the takeover mode) and filter in step 3, before the top-N cut. Motivation: an index-maintenance frag query occupied a top-15 slot on 11 of 11 production databases in both runs — it's genuinely volatile and expensive, so no honest ranking can drop it, and there was previously no way to exclude it. With @ignore_query_hashes it disappears on 11/11 (verified live).

3. mixed swings signal closes the classification gap

cpu_ratio >= 20 that fails both the parameter-sensitive test (>= 10x rows ratio) and the row-driven test (<= 2x rows ratio) previously produced no verdict at all — 25 of 162 surfaced shapes in the fleet run had a blank signals column, which reads as "nothing to see" on a row the mode itself just ranked top-15.

4. Ratio display caps

Floored mins produced cpu 2850350x, which stops carrying information; displays now cap at 9999+, and shapes that never returned a row read rows flat instead of rows 0x.

5. wait_stats column in the shape volatility summary

top_waits is silently omitted when wait stats capture is off (or pre-2017) — easy to misread as "no waits" (only 1 of the 11 prod databases had capture on, and the silent omission fooled the harness that found these issues). The summary now says which case applies, verified live on both a capture-on and capture-off database.

CI: rolling dev Install-All build (separable — happy to drop from this PR)

Install-All is intentionally only regenerated on main because dev rebuilds made every dev→main merge conflict inside the generated 2+ MB file. The new dev-installall-artifact.yml compiles it on dev pushes and publishes to a rolling dev-latest prerelease asset — nothing is ever committed to the branch, so merges stay clean, and a current dev build is gh release download dev-latest -p DarlingData.sql away. Context: the two wait-accounting fixes merged 8/04 weren't in any compiled build, which forced a manual overlay when updating a 54-instance fleet this week.

Tests

sp_QuickieStore/tests/run_tests.py: 157 → 166 passing (SQL Server 2022 container). New assertions: bidirectional hash include/ignore proof against the sniffed shape's actual query_hash value in detail rows, work-weighted ranked_on marker, wait_stats summary presence, and a sort_value no-leak guard.

Harness portability note observed while testing: classic ODBC sqlcmd (17.x, macOS homebrew mssql-tools) rejects the harness's -W -y 200 flag pair as mutually exclusive; go-sqlcmd (what CI installs) accepts it. Ran locally with SQLCMD_BIN pointed at go-sqlcmd v1.10.0. Left as-is, flagging in case a follow-up portability tweak is wanted.

🤖 Generated with Claude Code

erikdarlingdata and others added 3 commits August 4, 2026 13:09
Five changes driven by running the mode against a busy 11-database fleet:

- Work-weight the ranking: coefficient of variation alone is scale-blind,
  so a 3ms query swinging to 60ms outranked a 2s query swinging to 40s.
  Selection and presentation now rank on volatility * LOG10(10 + total
  work in the window for the ranked metric). On quiet systems the weight
  is near-uniform and behavior is effectively unchanged; on busy systems
  a shape must be both volatile and expensive to surface. The
  volatility_score column still shows the raw coefficient of variation;
  ranked_on says 'work-weighted' so the output describes itself.
- Honor the hash-based include/ignore lists: shapes are keyed by
  query_hash + query_plan_hash, so the lists map directly. Parsed locally
  in the mode block (the main filter section is never reached), filtered
  in step 3 before the top-N cut. In the fleet run an index-maintenance
  query burned a top-15 slot on 5 of 11 databases with no way to exclude
  it across runs.
- Close the signals classification gap: cpu_ratio >= 20 that fails both
  the parameter-sensitive test (>= 10x rows ratio) and the row-driven
  test (<= 2x rows ratio) previously produced no verdict at all; those
  shapes now read 'mixed swings (cpu Nx, rows Nx)'.
- Cap displayed ratios at 9999+ (floored mins produced values like
  'cpu 2850350x', which stops carrying information) and render zero-row
  shapes as 'rows flat' instead of 'rows 0x'.
- Explain top_waits availability in the shape volatility summary: the
  column is silently omitted when wait stats capture is off or pre-2017,
  which is easy to misread as no waits; a wait_stats summary column now
  says which case applies.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…t_stats summary in @find_parameter_sensitive

Nine new assertions: bidirectional hash include/ignore proof against the
sniffed shape's actual query_hash value in detail rows (ignore removes it,
include keeps only it, a hash nobody has returns zero shapes with the
summary as positive control), ranked_on carries the work-weighted marker,
the summary explains top_waits availability, and the internal sort_value
column does not leak into output. 166 total.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Install-All/DarlingData.sql is only regenerated and committed on main,
because rebuilding it on dev made every dev -> main merge conflict inside
the generated 2+ MB file. That leaves dev's tracked copy stale: the two
sp_QuickieStore wait-accounting fixes merged 8/04 were not in any compiled
build until release.

This workflow compiles Install-All on every dev push that touches SQL and
publishes it as an asset on a rolling 'dev-latest' prerelease. Nothing is
committed to the branch, so merges never see the file change, and a fresh
dev build is always one download away:

  gh release download dev-latest -p DarlingData.sql -R erikdarlingdata/DarlingData

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

claude Bot commented Aug 4, 2026

Copy link
Copy Markdown

Claude finished @erikdarlingdata's task in 4m 18s —— View job


Review: PR #854@find_parameter_sensitive dogfooding fixes

  • Gather context (read diff, changed files)
  • Review correctness (dynamic SQL, NULL handling, overflow safety)
  • Review SQL Server version compatibility
  • Review self-containment
  • Review style vs CLAUDE.md
  • Review CI workflow / test changes
  • Post final review

Looks good — no blocking issues found. This is a well-scoped, well-tested change. Details below.

Correctness

  • Hash filter placement is safe. The new #include_query_hashes / #ignore_query_hashes / #include_plan_hashes / #ignore_plan_hashes population (sp_QuickieStore.sql:7268-7349) runs inside the @find_parameter_sensitive mode block, and I traced control flow to confirm the main filter-processing cursor (sp_QuickieStore.sql:9343) is genuinely unreachable from this mode: the mode block always hits RETURN (line 9025) or GOTO DEBUG (line 9021 → label at line 16308), both of which skip past 9343. So there's no double-population/PK-violation risk on the shared temp tables.
  • Types line up. #ps_shape_stats.query_hash/query_plan_hash are binary(8), matching the computed query_hash/plan_hash columns on the filter temp tables (varchar(131) source + CONVERT(binary(8), ..., 1) PERSISTED), so the join predicates in step 3 (sp_QuickieStore.sql:7842-7881) compare like types.
  • Downstream propagation confirmed. All later steps (id staging, output insert, final ORDER BY) join through #ps_interesting, so a shape excluded in step 3 by the hash filters stays excluded everywhere downstream — no separate filter re-application needed.
  • mixed swings gap-closing logic checks out. Working through the boolean algebra: mixed swings requires cpu_ratio >= 20 AND max_cpu_ms >= 10 AND cpu_ratio < rows_ratio * 10 AND (rows_ratio < 20 OR cpu_ratio > rows_ratio * 2). The last clause is exactly the negation of row driven's condition (rows_ratio >= 20 AND cpu_ratio <= rows_ratio * 2), and the cpu_ratio < rows_ratio * 10 clause is the negation of parameter sensitive's. So the three signals are mutually exclusive and jointly close the previously-blank-verdict gap described in the PR.
  • 9999+ cap and rows flat are applied consistently across all five ratio displays (cpu, rows, duration, memory, tempdb), and rows flat correctly keys off ps.max_rows = 0 (which is exactly the case that previously rendered as a meaningless 0x).
  • LOG10 argument is always safe10. + ISNULL(work_metric, 0.) can't go non-positive since all the underlying work metrics (cpu ms, duration ms, mb * executions) are non-negative.
  • sort_value non-leak confirmed — it's added to #ps_output and used only in the final dynamic SQL ORDER BY (sp_QuickieStore.sql:8977); the actual output column list never selects it, matching the new test assertion.

Version compatibility

  • LOG10 (2008+), no new syntax introduced. The wait_stats summary correctly reuses the existing @new (SQL 2017+ flag) and @query_store_waits_enabled gating pattern already used elsewhere in the procedure (e.g. lines 5856-5857).

Self-containment

  • No new external objects. The hash-list parsing block duplicates the existing REPLACE(...CHAR(10)/CHAR(13)...) cleanup and @string_split_strings splitter idiom verbatim from the main filter section rather than factoring out a shared helper — reasonable given the stated constraint that stored procedures must stay self-contained, and the takeover-mode structure means the main filter path truly isn't reachable here.

Style

  • New code (mode-local hash filter block, ranking CASE/LOG10 expression, ratio-cap CASE expressions) follows the CLAUDE.md formatting conventions already used throughout the file (aligned AND/OR, 1/0 in EXISTS, trailing commas, block comments). Nothing to flag.
  • No @version / @version_date changes, consistent with this being a non-release PR.

CI workflow (dev-installall-artifact.yml)

  • Reasonable approach to the stated problem (rolling dev-latest prerelease asset, nothing committed to dev). git push -f origin dev-latest is a force-push, but it's scoped to a dedicated rolling tag used only for this artifact, which is the standard pattern for "latest" release tags — not a concern.

Tests

  • New assertions in run_tests.py reuse existing helpers (PS_SUMMARY_MARKER, ps_detail_rows) correctly, and the bidirectional include/ignore proof against the actual sniffed query_hash value (rather than object name) is a good, precise test design given the PR's own point about object names appearing in other shapes' text.

No changes requested.

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