Related: #3189, #3210, #3216 (the touch guard). The guard's doc comment names write volume as its trade and records no measurement of it at the 6-hour width.
Problem
QueryStoreLivenessTouchGuard sets how often the touch-and-probe statements refresh last_seen on query_store_plan_map and query_store_text: GuardHours = StampSkewMarginHours / MarginShareDivisor = 24 / 4 = 6 h.
The comment on MarginShareDivisor states the divisor is "CHOSEN, not measured". It also says what the divisor trades: "WRITE VOLUME against stamp freshness". last_seen is indexed on both tables, so every touch is a non-HOT update: a new heap tuple, new b-tree entries, a dead tuple, and WAL for all of it. With data checksums and 5-minute checkpoints, most of those pages are imaged in full. Here is the write-volume side, measured.
Measured
Production SQL Server store A (43 servers, the Query Store–ingesting store), build 3.8.0-nightly.20260924.467. Steady window 2026-09-25 04:45–05:41Z (55.6 min), no restart inside.
pg_stat_statements:
- plan-map touch-and-probe: 310 calls, 399,287 WAL records, 128,191 page images, 0.74 GB;
- text touch-and-probe: 308 calls, 280,439 records, 75,733 page images, 0.53 GB;
- together 1.28 GB = 13.5% of the store's WAL (9.45 GB), and 1.71 GB of buffers dirtied.
pg_stat_all_tables, same window: query_store_plan_map 82,880 updates (0 HOT), query_store_text 79,836 (31 HOT). That's ~163 k touches an hour, ~3.9 M a day, which matches the 6-hour guard over the referenced set.
pg_waldump per-relation sample (1.1 GB at 05:41Z): the query_store_text heap 5.6%, the query_store_plan_map heap 4.1%, their pkeys 1.1%. Most of it is page images and hint images.
- Per day: ~33 GB of WAL and ~44 GB of dirtied pages written back, on a store generating ~250–350 GB of WAL a day.
- The window straddling the 04:04Z restart had 3.73 GB, 23% of WAL: the first cycle after a start probes and touches the whole referenced set at once.
- Store B holds Query Store
READ_ONLY replicas and stores nothing here.
Where
Darling/PerformanceMonitor.Darling.Storage/QueryStoreLivenessTouchGuard.cs: MarginShareDivisor :133, GuardHours :141, StampSkewMarginDays :102.
Darling/PerformanceMonitor.Darling.Storage/QueryStorePlanMap.cs:170, TouchAndProbeSql; last_seen index :68.
Darling/PerformanceMonitor.Darling.Storage/QueryStoreTextStore.cs:114, TouchAndProbeSql; last_seen index :65.
Fix shape
Pick by what the margin can afford. All three cut writes roughly in proportion.
- Widen the guard.
- Raise
QueryStorePlanMap.PruneMarginDays from 1 to 2, matching the text store. Then MarginShareDivisor 4 gives a 12 h guard and halves the touches, with the same ¼-of-margin headroom the comment asks for.
- Or keep the margin and move the divisor to 2.
- Make the touch HOT.
- Drop the
last_seen indexes and prune by a batched sequential scan on the daily purge (the tables are keyed stores, not hypertables). With a fillfactor below 100, a last_seen-only update stays on-page with no index entries.
- Measure the purge's cost before and after.
- Spread the post-restart wave. The first cycle touches everything at once, which is 23% of WAL in that hour. It could jitter the touch by server or by key hash.
- Pin:
- the guard derivation test already exists; add the measured write volume to the divisor's comment, so the next reader has both sides of the trade;
- if the index is dropped, a live test that a touch is HOT (
n_tup_hot_upd increments).
Related: #3189, #3210, #3216 (the touch guard). The guard's doc comment names write volume as its trade and records no measurement of it at the 6-hour width.
Problem
QueryStoreLivenessTouchGuardsets how often the touch-and-probe statements refreshlast_seenonquery_store_plan_mapandquery_store_text:GuardHours = StampSkewMarginHours / MarginShareDivisor= 24 / 4 = 6 h.The comment on
MarginShareDivisorstates the divisor is "CHOSEN, not measured". It also says what the divisor trades: "WRITE VOLUME against stamp freshness".last_seenis indexed on both tables, so every touch is a non-HOT update: a new heap tuple, new b-tree entries, a dead tuple, and WAL for all of it. With data checksums and 5-minute checkpoints, most of those pages are imaged in full. Here is the write-volume side, measured.Measured
Production SQL Server store A (43 servers, the Query Store–ingesting store), build 3.8.0-nightly.20260924.467. Steady window 2026-09-25 04:45–05:41Z (55.6 min), no restart inside.
pg_stat_statements:pg_stat_all_tables, same window:query_store_plan_map82,880 updates (0 HOT),query_store_text79,836 (31 HOT). That's ~163 k touches an hour, ~3.9 M a day, which matches the 6-hour guard over the referenced set.pg_waldumpper-relation sample (1.1 GB at 05:41Z): thequery_store_textheap 5.6%, thequery_store_plan_mapheap 4.1%, their pkeys 1.1%. Most of it is page images and hint images.READ_ONLYreplicas and stores nothing here.Where
Darling/PerformanceMonitor.Darling.Storage/QueryStoreLivenessTouchGuard.cs:MarginShareDivisor:133,GuardHours:141,StampSkewMarginDays:102.Darling/PerformanceMonitor.Darling.Storage/QueryStorePlanMap.cs:170,TouchAndProbeSql;last_seenindex :68.Darling/PerformanceMonitor.Darling.Storage/QueryStoreTextStore.cs:114,TouchAndProbeSql;last_seenindex :65.Fix shape
Pick by what the margin can afford. All three cut writes roughly in proportion.
QueryStorePlanMap.PruneMarginDaysfrom 1 to 2, matching the text store. ThenMarginShareDivisor4 gives a 12 h guard and halves the touches, with the same ¼-of-margin headroom the comment asks for.last_seenindexes and prune by a batched sequential scan on the daily purge (the tables are keyed stores, not hypertables). With afillfactorbelow 100, alast_seen-only update stays on-page with no index entries.n_tup_hot_updincrements).