Store new plan XML gzip-compressed in the plan dimension (#2069) - #2072
Conversation
query_plan_dim was 101 GB of the production store's 147 GB (69%) -- 6.5M distinct plans, lz4-TOASTed at a measured 9.2x. A bake-off on the store's own live content measured application-side gzip (Optimal) at 14.0x against lz4's 8.9x on the identical sample, projecting the dimension's steady state to ~64 GB. gzip rather than zstd because PG18 TOAST offers only pglz/lz4 (verified against the binary) and .NET ships gzip natively; zstd tracked in #2071. Write path: the dimension writer compresses each distinct plan once into a new query_plan_gz bytea column (V54; text column now nullable and left NULL on new rows). The content digest is still computed over the UNCOMPRESSED text, so identity, dedup, fact-row digests, and every has-plan presence flag survive the format change. Old rows keep their text; the dimension GC retires them on its normal horizon (~9 days measured), so the store converts itself with no rewrite and no peak-disk spike. Read path: every plan reader resolves text-else-gz through one rule (PayloadDimensions.ResolveContent) -- the two service MCP reads, the two viewer keyed plan fetches, both FinOps workload reads (MAX aggregate + correlated sample subquery), and the actual-plan command's stored-query resolver (its other two source kinds bind NULL::bytea so the reader stays uniform). The resolving view appends query_plan_gz after the digests (replace-only-appends), and V38 pre-adds the column before the view for the partial-apply re-run case, pinned by test. Disclosed loss: v_query_stats.query_plan_xml is NULL for post-V54 plans -- PG cannot gunzip in SQL; ad-hoc consumers read query_plan_gz (a plain RFC 1952 gzip member, gunzip-recoverable). Product surfaces are unaffected. Ladder ceremony: StorageVersion 54, 37th viewer probe sentinel (query_plan_dim.query_plan_gz) with newest-first map arm, version pins swept (observability, PVS, full-sentinel, live-migration count), the last-view-definer tripwire moves 51 -> 54, and the view/DDL/upsert shape pins gain the gz column. New codec tests pin round-trip (including non-ASCII), real-gzip magic, absent-resolves-to-null, text-first preference, and digest stability; the live write-path test now pins the gz contract end to end. Closes #2069 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The V54 doc+const had been inserted between V53's doc comment and V53Sql, stacking the two summaries (DocCommentHygieneTests caught it) -- moved below V53Sql so V53's doc reattaches to its member. StoreSelfMetricsTests newest-rung pins (Scripts[^1], SchemaVersion, RequiredStoreSchemaVersion) move 53 -> 54; its V53 map-arm assertion stays 53 and now documents that hasPlanDimGzip defaults false. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review: #2072 — gzip-compressed plan-dimension contentOverall this is a carefully executed, well-tested storage change: additive-only migration (V54), digest computed over uncompressed text so identity/dedup survive the format change, a single Correctness bug: two read seams still filter on the bare
|
The live migration test asserted every dim column NOT NULL; V54 makes query_plan_dim.query_plan_xml nullable by design (new rows are gz-only), so the nullability expectation is now per-column (text dim and digest/last_seen stay NOT NULL) and the test additionally pins the gz bytea column's presence and nullability. The collector-runner test's plan-reachability helpers gain the gz arm and resolve text-else-gz like every product reader -- data written through the real runner now lands as gzip bytes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ReviewScope: gzip-compresses new plan XML into CorrectnessTraced every touched read path (service MCP reads, viewer keyed plan fetches, FinOps The V38 pre-add ( One thing worth naming explicitly (not a blocker, since the PR description already discloses the mechanism): the Performance
SecurityNo new user-input handling; the compressed bytes come from the service's own SQL Server plan capture and are only ever decompressed by the same service. No injection surface changes — all new SQL is compile-time constant strings with parameterized binds, consistent with the rest of the codebase. Style / testsNew SQL literals use No blocking issues found. |
Closes #2069.
What
New execution plans are stored gzip-compressed (
query_plan_gzbytea, V54) inquery_plan_dim; the dimension's text column goes nullable and stays NULL on new rows. Every plan reader resolves text-else-gz through one rule (PayloadDimensions.ResolveContent).Why
The #2068 self-metrics sweep quantified it:
query_plan_dimis 101 GB of the production store's 147 GB (69%). lz4 TOAST already compresses it 9.2x; a bake-off on the store's own live content measured application-side gzip (Optimal) at 14.0x vs lz4's 8.9x on the identical sample — projected steady state ~64 GB. gzip, not zstd: PG18 TOAST enumvals arepglz, lz4only (verified against the shipped binary), and .NET ships gzip natively. zstd stays tracked in #2071.Design properties
has_query_planpresence all survive the format change (pinned by test).NULL::byteaso the shared reader stays uniform).v_query_statsappendsquery_plan_gzafter the digests (replace-only-appends rule); V38 pre-adds the column before the view for the partial-apply re-run case (pinned, same reasoning as the digest pre-adds).query_plan_xmlon post-V54 plans; the bytes inquery_plan_gzare a plain RFC 1952 gzip member (gunzipalone recovers them). Product surfaces unaffected. The live write-path test now pins this contract end to end.Ladder ceremony
StorageVersion 54; 37th viewer probe sentinel (
query_plan_dim.query_plan_gz) with newest-first arm; pins swept: observability x2, PVS x3, full-sentinel 36->37, live-migration applied 8->9, last-view-definer tripwire 51->54, view/DDL/upsert shape pins.Tests
New: codec round-trip incl. non-ASCII + real-gzip magic, absent->null, text-first preference, digest stability across the format change, V38 gz-before-view ordering. Updated: the SQL shape pins above + the live write-path round-trip (now asserts NULL text + gz bytes in both the dim and the view).
Lite is untouched (no dims, no plan capture).
🤖 Generated with Claude Code