Repository navigation
Decompress each Query Store plan/text once in the by-ids fetch (~4.5x) (#2675) - #2676
Conversation
The by-ids plan and text fetches measured the decompressed size with DATALENGTH and emitted the payload from the same inlined CTE. Because a CTE inlines rather than materialises, CONVERT(nvarchar(max), query_plan) -- the decompression, which sys.query_store_plan performs on every access -- re-ran for each of the three references (byte measure, running total, passthrough), decompressing every plan ~3x. Measured on a SQL Server 2025 rig, 1,500 plans x ~68 KB plan XML: 2,440 ms CPU the old way vs ~540 ms decompressing once -- ~4.5x, on the single most expensive operation in the heaviest collector. The wrapped sp_executesql form was verified end to end on the rig (3 ids -> 3 rows, correct columns, no error). Both fetches now materialise the payload to a #temp once (SET NOCOUNT ON so the SELECT INTO yields no result set to the reader), then run the UNCHANGED byte-budget cut over the stored column. Query text got the same shape -- not compressed, but the repeated nvarchar(max) reads scale with real text size, where it is the larger of the two phases in the field. No behaviour change: same missing-only set, watermark, budget, rows, order. Tests updated: the alias moved c -> p, plus pins that the decompression CONVERT appears exactly once and the fetch materialises to #temp, so a revert to the inline triple-reference form fails the build. Addresses #2675 (primary perf fix; the count-based cut for forward progress and dropping the residual runtime-query text join remain in the issue). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
| query_plan_text = CONVERT(nvarchar(max), qsp.query_plan) | ||
| INTO #plan_fetch | ||
| FROM sys.query_store_plan AS qsp | ||
| WHERE qsp.plan_id IN ({idList}); |
There was a problem hiding this comment.
This new staging statement (SELECT ... INTO #plan_fetch ...) is missing OPTION(RECOMPILE). Only the second SELECT in the batch (line ~1257) carries the hint.
Per CONTRIBUTING.md: "OPTION(RECOMPILE) on collector queries... A statement added to an existing batch needs its own hint — one on a neighbouring statement does not cover it." This exact scenario (splitting a batch into a staging SELECT INTO + final SELECT) was already caught in review once in this same file — see the comment at line ~904: "BOTH statements carry OPTION(RECOMPILE) (review catch): split out on its own, the staging statement would otherwise be cached via sp_executesql's parameterized text and sniffed across live vs backfill windows of wildly different selectivity."
Suggest adding OPTION(RECOMPILE) to this SELECT INTO as well. Also worth noting neither the Darling nor Lite test suites check for more than one occurrence of "OPTION(RECOMPILE)", so this regression wasn't caught by the pinned tests.
| FROM sys.query_store_query AS qsq | ||
| JOIN sys.query_store_query_text AS qst | ||
| ON qst.query_text_id = qsq.query_text_id | ||
| WHERE qsq.query_id IN ({idList}); |
There was a problem hiding this comment.
Same issue as the plan-fetch staging statement: this SELECT ... INTO #text_fetch ... lacks its own OPTION(RECOMPILE). Only the final SELECT (line ~1366) has the hint. Per CONTRIBUTING.md, each statement in a collector batch needs its own hint — see the same finding on the plan-fetch method above.
|
Reviewed the decompress-once change. The approach is sound (materializing the decompressed plan/text to a One issue found, posted as inline comments: both new staging |
Addresses #2675 — the primary, measured performance fix.
What was slow
The by-ids plan/text fetch measured a plan's decompressed size with
DATALENGTHand emitted the plan from the same inlined CTE. Because a CTE inlines rather than materialises,CONVERT(nvarchar(max), qsp.query_plan)— the decompression, whichsys.query_store_planperforms on every access — re-ran for each of its three references (the byte measure, the running-total SUM, and the passthrough). Every plan was decompressed ~3×.Measured (local SQL Server 2025 rig, 1,500 plans × ~68 KB plan XML, warm CPU)
~4.5× on the single most expensive operation in the heaviest collector. The exact
sp_executesql-wrapped form was verified end-to-end on the rig (3 ids → 3 rows, correct columns, no error).The change
Both by-ids fetches now materialise the payload to a
#temponce (SET NOCOUNT ONso theSELECT INTOreturns no result set to the reader), then run the unchanged byte-budget cut over the stored column.DATALENGTHover the temp reads a stored LOB length — no further decompression. Query text got the same shape (not compressed, but the repeatednvarchar(max)reads scale with real text size, where it's the larger of the two phases in the field).No behaviour change: same missing-only set, same watermark, same byte budget, same rows, same order. All existing SQL-pinning assertions hold except one alias (
c→p); added pins assert the decompressionCONVERTappears exactly once and the fetch materialises to#temp, so a revert to the inline form fails the build.Still open in #2675 (follow-ups)
sys.query_store_query_textjoin from the main runtime-stats query when text is fetched separately.🤖 Generated with Claude Code