Skip to content

Darling: three-tier retention (raw 4d / hourly CAGGs 21d / daily CAGGs kept) - #1623

Merged
erikdarlingdata merged 2 commits into
devfrom
feature/darling-retention-tiers
Jul 23, 2026
Merged

erikdarlingdata merged 2 commits into
devfrom
feature/darling-retention-tiers

Conversation

@erikdarlingdata

Copy link
Copy Markdown
Owner

What

Builds the retention layer on top of the hourly CAGGs ([#1621]). Instead of a flat "drop raw after N days," three tiers so nothing is ever fully lost, just coarsened. All idempotent RUNTIME setup in TimescaleSupport, mirroring the hourly CAGGs (not a versioned migration).

Tier 1 — raw, 4 days. add_retention_policy on query_stats / procedure_stats / query_store_stats. 4d = 1 day past the hourly CAGG's own 3-day refresh window, so the raw drop never outruns the aggregate that preserves it.

Tier 2 — hourly CAGGs, 21 days. Adds query_store_stats_hourly (built now, ahead of any writable-Query-Store primary — empty on a read-only replica until one is added), plus add_retention_policy at 21d on all three hourly CAGGs.

Tier 3 — daily CAGGs, kept indefinitely. query_stats_daily / procedure_stats_daily as hierarchical CAGGs sourced from the hourly CAGGs (not raw — 2.28.1 supports CAGG-on-CAGG), 1-day refresh cadence. No retention policy — this is the coarsened, never-lost tier.

Correctness note

The daily GROUP BY uses the explicit time_bucket('1 day', bucket) expression, not the bare bucket alias. A bare bucket in GROUP BY binds to the hourly source column under Postgres's input-column-wins ambiguity rule, which would group by hour, not day. (Deviation from the literal spec SQL, for correctness.)

Ordering safety

Each tier's retention horizon stays comfortably past the next tier's 3-day refresh start (4d raw vs 3d hourly refresh; 21d hourly vs 3d daily refresh), so a drop never removes history the next tier hasn't materialized. add_retention_policy is a chunk-level DROP, so no off-hours window needed (unlike the CAGG backfill).

Cold-start caveat (documented on EnsureRetentionPoliciesAsync): on an EXISTING store holding raw older than the hourly CAGG has materialized, the first raw drop can remove buckets the CAGG never captured. Fresh installs are safe automatically (nothing is older than 4 days until the CAGG has been materializing that long). For an existing store (the field box), backfill the hourly CAGGs past the raw horizon before the retention runs — flagged in the client upgrade prompt.

Deferred

query_store_stats_daily — added the same way once query_store_stats_hourly has real data from a writable primary to source from.

Tests

TimescaleContinuousAggregateTests (9 pin tests): the three hourly shapes, the two hierarchical daily shapes with the explicit-time_bucket GROUP BY, the daily policy cadence, and the retention tiers/buffers. Live execution validated on DARLING01 (real 2.28.1) rather than the shared gated-live DB, to avoid cross-test interference on that suite.

🤖 Generated with Claude Code

erikdarlingdata and others added 2 commits July 23, 2026 11:17
…Gs kept

Builds the retention layer on top of the hourly CAGGs (#1621): downsample and bucketize
past the raw hot window instead of dropping outright, so nothing is fully lost, just
coarsened. All idempotent RUNTIME setup in TimescaleSupport, mirroring the hourly CAGGs.

- query_store_stats_hourly CAGG (built ahead of a writable-QS primary; empty on a
  read-only replica until one is added).
- Hierarchical DAILY CAGGs query_stats_daily / procedure_stats_daily, sourced from the
  hourly CAGGs (not raw; 2.28.1 supports CAGG-on-CAGG), 1-day refresh cadence. GROUP BY
  uses the explicit time_bucket('1 day', bucket) expression, NOT the bare `bucket` alias
  (which binds to the hourly source column under Postgres's input-column-wins rule and
  would group by hour, not day).
- Tiered retention via add_retention_policy: raw tables 4d, hourly CAGGs 21d; the daily
  CAGGs get no policy (kept indefinitely). Each tier's horizon stays past the next tier's
  3-day refresh start, so a drop never outruns the aggregate preserving it. Wired into
  DarlingWorker after the CAGG step.

query_store_stats_daily is deferred until QS-hourly has real data to source from.

Cold-start caveat (documented on EnsureRetentionPoliciesAsync): on an EXISTING store with
raw older than the hourly CAGG has materialized, the first raw drop can remove buckets the
CAGG never captured — backfill the hourly CAGGs past the raw horizon before retention runs.
Fresh installs are safe automatically.

Pinned by TimescaleContinuousAggregateTests (9 tests).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@erikdarlingdata
erikdarlingdata merged commit 772577f into dev Jul 23, 2026
4 of 5 checks passed
@erikdarlingdata
erikdarlingdata deleted the feature/darling-retention-tiers branch July 23, 2026 18:22
argpna pushed a commit to argpna/PerformanceMonitor that referenced this pull request Jul 24, 2026
Corrects the erikdarlingdata#1623 CAGG shapes so the composer can route to them (the read-routing
follow-up), while the CAGGs are still empty (QS, read-only replica) or a day or two old.

- query_store_stats_hourly: regrouped from query_id/plan_id to the composer's QS dims
  (module_name/query_hash) + execution-WEIGHTED sums (sum(avg_* * execution_count)) so
  the composer's weighted mean composes EXACTLY (duration_us_weighted_sum /
  execution_count_sum), not an avg-of-avgs. This is the shape a writable-Query-Store
  primary needs — QS composer panels were unroutable and the weighted mean
  unreconstructable as erikdarlingdata#1623 built it.
- procedure_stats hourly + daily: added schema_name to the grouping (a composer
  dimension that had been dropped, so schema_name panels couldn't route).
- DropStaleContinuousAggregatesAsync (wired before the ensure sweep): CREATE ... IF NOT
  EXISTS cannot ALTER a CAGG, so an existing old-shape CAGG is dropped (detected
  structurally: old QS still has query_id, old procedure_stats lacks schema_name) and
  recreated in the new shape. No-op once reshaped / on a fresh store; failure-isolated.

query_stats CAGGs are unchanged. object_name-on-query_stats routing is deliberately
deferred (would need sql_handle on both big CAGGs for a read-time join — bloats the
tables the CAGGs exist to shrink; revisit with a retained module map if it matters).

Pinned by TimescaleContinuousAggregateTests (updated for the new shapes).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant