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
{{ message }}
Repository navigation
The per-interval Query Store tables' daily purge full-scans them: minutes per call on a fleet store, with no index on first_execution_time #4608
The daily purge of collect.query_store_interval_latest (V143) and collect.query_store_interval_wide (V145) costs minutes per statement on a fleet store, even while it has nothing to delete. On one production monitoring store (pg_stat_statements since 2026-09-24 ~22:00Z):
table
total / heap
rows
purge DELETE calls
rows deleted
mean
max
query_store_interval_latest
8.4 GB / 6.4 GB
18.1 M
5
0
70.6 s
160.9 s
query_store_interval_wide
10 GB
9.1 M
3
0
23.5 s
57.3 s
Both tables are still inside their horizons (15 and 9 days), so no row is old enough yet. The no-op purge already costs up to 2.7 minutes a call, and the cost grows with the table. Once real deletes start, TimeSlicedDeleteSql repeats it for every slice.
Why
The purge shape.DarlingRetention.PurgeAsync purges both with TimeSlicedDeleteSql(table, "first_execution_time") (Darling/PerformanceMonitor.Darling.Service/DarlingRetention.cs ~738–770):
DELETEFROM t WHERE first_execution_time < $1AND first_execution_time >= (SELECTmin(first_execution_time) FROM t WHERE first_execution_time < $1)
AND first_execution_time < (SELECTmin(first_execution_time) FROM t WHERE first_execution_time < $1) + INTERVAL '1 days'
The missing index. Neither table has an index leading on first_execution_time. The only index on each is the unique identity key, where it comes last (PgMigrations.cs ~2129–2139 and ~2306–2317). Both are plain tables, not hypertables, so there's no chunk exclusion either.
So each statement is a full scan, up to three with the two min() subqueries. The same store's stats show 9 seq scans reading 60 M rows on query_store_interval_latest.
The read gate has the same problem.QueryStoreIntervalWide's MIN(first_execution_time) floor has the same shape (DarlingRetention.cs ~762, "The table floor the read gate checks").
Fix direction
One of the two, chosen by a rig measurement at a fleet-sized row count, and stated on the PR:
An index on first_execution_time for both tables (a migration rung). That makes the purge's slices and the read gate's floor index range scans.
first_execution_time never changes for a row, so the tables' upsert keeps its HOT updates.
Activity
Plain wording in the V153 rung's comment (#4608)
Keep V152's live pins true once a later rung adds its own probe senti…
Store rungs V153 and V154: index the per-interval Query Store tables'…
erikdarlingdata commentedon Sep 28, 2026
Closed by the watcher: delivered in PR #4615, merged to dev.