You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Shared collectors (QueryStoreCollector), both apps
Field evidence (dogfood, 2026-08-10)
A 4-core multi-tenant box post-resize-restart (all plans recompiled) + a freshly restored tenant catalog: every database hit MaxTextBytesPerDatabase (64MB, compile-time const) on every pass, at 44–109s of SQL time per database, ~380–427s per full server pass, repeating every cycle. The staged shape (#2134) worked exactly as designed — bounded, resumable, no wedge — but the drain tax is large and highly visible (60–100s PerformanceMonitorLite statements in sp_WhoIsActive on a prod primary).
Why passes cost this much
The per-pass ceiling is generous and fixed: MaxTextBytesPerDatabase = 64 * 1024 * 1024 (QueryStoreCollector.cs:327). 64MB of QS text/plan extraction ≈ 45–110s on small hardware because CONVERT(nvarchar(max), qsp.query_plan) decompresses plan XML server-side — that's the dominant CPU term.
No cross-pass plan memory: query_plan_text ships once per plan per pass (ROW_NUMBER() OVER (PARTITION BY qsp.plan_id ...) = 1). A hot plan re-decompresses and re-ships every cycle; the store dedups on arrival — after the server already paid the extraction. In a recompile storm, most of every 64MB is plans the store has.
Proposed levers
(a) Budget knob: make the per-database text byte budget a store setting (per-server override ideal), default 64MB, floor ~4MB — the Expose the hardcoded self-alert thresholds as store-backed settings #2107 store-backed knob pattern. Small boxes drain in 15–25s bites instead of 60–110s ones; same total work.
(b) Known-hash skip (the big win): qsp.query_plan_hash is available before the CONVERT. Maintain the recently-shipped hash set per (server, database) in collector state, pass it in (temp table / TVP), and extend the ship condition to ROW_NUMBER = 1 AND qsp.query_plan_hash NOT IN (SELECT h FROM #known). Recompile storms then ship stats rows but almost no plan payload. Caveat to design around: query_plan_hash is a plan-shape hash, not a full-XML digest — distinct XML can rarely share a hash; acceptable for perf telemetry but should be a documented tradeoff (or refresh hashes older than N days).
The two compose: (b) shrinks steady-state and storm payloads; (a) bounds the worst case on hardware that can't afford big bites.
Component
Shared collectors (QueryStoreCollector), both apps
Field evidence (dogfood, 2026-08-10)
A 4-core multi-tenant box post-resize-restart (all plans recompiled) + a freshly restored tenant catalog: every database hit
MaxTextBytesPerDatabase(64MB, compile-time const) on every pass, at 44–109s of SQL time per database, ~380–427s per full server pass, repeating every cycle. The staged shape (#2134) worked exactly as designed — bounded, resumable, no wedge — but the drain tax is large and highly visible (60–100sPerformanceMonitorLitestatements in sp_WhoIsActive on a prod primary).Why passes cost this much
MaxTextBytesPerDatabase = 64 * 1024 * 1024(QueryStoreCollector.cs:327). 64MB of QS text/plan extraction ≈ 45–110s on small hardware becauseCONVERT(nvarchar(max), qsp.query_plan)decompresses plan XML server-side — that's the dominant CPU term.query_plan_textships once per plan per pass (ROW_NUMBER() OVER (PARTITION BY qsp.plan_id ...) = 1). A hot plan re-decompresses and re-ships every cycle; the store dedups on arrival — after the server already paid the extraction. In a recompile storm, most of every 64MB is plans the store has.Proposed levers
qsp.query_plan_hashis available before the CONVERT. Maintain the recently-shipped hash set per (server, database) in collector state, pass it in (temp table / TVP), and extend the ship condition toROW_NUMBER = 1 AND qsp.query_plan_hash NOT IN (SELECT h FROM #known). Recompile storms then ship stats rows but almost no plan payload. Caveat to design around:query_plan_hashis a plan-shape hash, not a full-XML digest — distinct XML can rarely share a hash; acceptable for perf telemetry but should be a documented tradeoff (or refresh hashes older than N days).The two compose: (b) shrinks steady-state and storm payloads; (a) bounds the worst case on hardware that can't afford big bites.