Problem
The per-pass signals_latest upkeep (captureRollupDelta + foldSignalsRollup, run inside every span transaction) dominates materializer pass cost: ~300s at afternoon peak vs ~10s at dawn (dq_materializer_phase_seconds shows commit at ~1800–2200s/hour; decode is ~10s/hour). The fold is O(batch) in rows but O(day-partition files × 3) in I/O:
- The
recency/locrec CTEs re-derive latest-per-(subject, name) from lake.signals with a correlated bound (s.timestamp >= p.prev_ts), which cannot drive static partition pruning — no constant day = predicate exists in the query — so each pass re-opens roughly the affected buckets' current-day files.
- The scan happens three times per pass (recency, locrec, and
captureRollupDelta's NOT-EXISTS probe).
- File opens are ~0.5s-class over httpfs, and the day partition's file count grows all day.
Result: the daily pass-duration sawtooth, decode ceiling below business-hours ingest (~255 snapshots/h), and the 2026-08-05/06 backlogs. Vehicle count is irrelevant to the cost — it's priced in file opens.
The fold's correctness machinery (recompute-equivalence, cloud_event_id tie-breaks against the base, QUALIFY dedup matching the read path, self-healing recency) exists to keep the table continuously exact — but the table stopped being a serving surface when the KV shipped: LATEST_KV_READ_MODE=serve in prod, negative path included. Its remaining jobs are KV bootstrap/reconcile source and fallback. We're paying the old system's ongoing cost for a backup copy.
Design
Remove the fold from the span transaction; refresh the rollup once daily from a watermark.
- Span commit keeps only: base inserts (signals/events) + KV publish + cursor advance. No rollup reads/writes.
- Daily refresh step (in the materializer loop — preserves the single-writer invariant; NOT a separate pod/cron):
- Runs after the UTC-midnight partition rollover once the cursor has settled past it (~03:30–04:00 UTC — also this node's traffic trough).
- Folds
lake.signals WHERE timestamp >= <watermark> into the rollup with a constant watermark literal → static file pruning, one complete settled partition in steady state.
- Watermark-driven and idempotent: a missed run folds two days next time; no operator memory needed. Watermark stored alongside the rollup (same transaction).
- Simple
arg_max/QUALIFY over (rollup ∪ tail) — the per-pass fold's tie-break/delta machinery can be dropped or reused; equivalence bar is unchanged (differential test stays).
- Coverage reconcile becomes watermark-aware — the sharp edge. It currently diffs KV against the rollup assuming freshness; against a daily rollup it must diff against (rollup ∪ signals-since-watermark), or bound comparisons at the watermark. Otherwise reconcile "repairs" the KV backwards up to a day. Same care as the CAS/multi-writer rules on the bucket.
BootstrapFromRollup gains a tail replay: bootstrap from rollup (≤24h stale), then fold signals since watermark — constant predicate, 1–2 partitions, minutes, and stays bounded forever (vs O(history) pure-drop rebuild that grows with retention).
Cadence rationale (why daily, not finer/coarser)
The day partition is the cost quantum. Hourly refreshes rescan today-so-far repeatedly during business hours (recreating the problem, milder); weekly saves nothing per-day-covered and grows the recovery/reconcile tail 7x. Daily anchored to rollover folds exactly one complete settled partition. The rollup has no freshness SLA of its own anymore — its only consumer is disaster recovery, which is minutes-fast at any cadence.
Expected wins
- Span passes lose their dominant, day-length-dependent term → sawtooth flattens, decode ceiling stops depending on time of day.
MATERIALIZER_MAX_SNAPSHOT_SPAN=32 (dimo-node#123 stopgap) can return to 16.
signals_latest churn drops from ~288 rewrite cycles/day to 1 → the delete-churn fragmentation problem (din#13's rewrite_data_files motivation) essentially disappears at the source; din maintenance cycles get cheaper.
Out of scope / later
events_latest: same pattern, no KV equivalent yet — untouched here; measure its share and consider the same treatment (or a KV) separately.
- Dropping the table entirely: viable phase 2 once the KV has solo-serving history; the daily job and
lake_rollup.go fallback removal make the eventual drop trivial. Not now — the rollup-as-recovery-snapshot keeps KV rebuild O(1 day) instead of O(history).
LAKE_REBUILD_ROLLUP_ON_BOOT / RecomputeRollup stay as the deep-DR path (unchanged cost class).
Migration order (each step independently shippable)
- Add watermark column/row + daily refresh step (flag-gated), per-pass fold still on: differential-compare daily output vs incremental for a few days.
- Make coverage reconcile + bootstrap watermark-aware.
- Flip the flag: fold off, daily on. Watch
dq_lake_latest_kv_* coverage metrics and span durations.
- Remove the per-pass fold code +
MATERIALIZER_MAX_SNAPSHOT_SPAN back to default; retire lake_rollup.go fallback when confident.
Problem
The per-pass
signals_latestupkeep (captureRollupDelta+foldSignalsRollup, run inside every span transaction) dominates materializer pass cost: ~300s at afternoon peak vs ~10s at dawn (dq_materializer_phase_secondsshows commit at ~1800–2200s/hour; decode is ~10s/hour). The fold is O(batch) in rows but O(day-partition files × 3) in I/O:recency/locrecCTEs re-derive latest-per-(subject, name) fromlake.signalswith a correlated bound (s.timestamp >= p.prev_ts), which cannot drive static partition pruning — no constantday =predicate exists in the query — so each pass re-opens roughly the affected buckets' current-day files.captureRollupDelta's NOT-EXISTS probe).Result: the daily pass-duration sawtooth, decode ceiling below business-hours ingest (~255 snapshots/h), and the 2026-08-05/06 backlogs. Vehicle count is irrelevant to the cost — it's priced in file opens.
The fold's correctness machinery (recompute-equivalence, cloud_event_id tie-breaks against the base, QUALIFY dedup matching the read path, self-healing recency) exists to keep the table continuously exact — but the table stopped being a serving surface when the KV shipped:
LATEST_KV_READ_MODE=servein prod, negative path included. Its remaining jobs are KV bootstrap/reconcile source and fallback. We're paying the old system's ongoing cost for a backup copy.Design
Remove the fold from the span transaction; refresh the rollup once daily from a watermark.
lake.signals WHERE timestamp >= <watermark>into the rollup with a constant watermark literal → static file pruning, one complete settled partition in steady state.arg_max/QUALIFYover (rollup ∪ tail) — the per-pass fold's tie-break/delta machinery can be dropped or reused; equivalence bar is unchanged (differential test stays).BootstrapFromRollupgains a tail replay: bootstrap from rollup (≤24h stale), then foldsignalssince watermark — constant predicate, 1–2 partitions, minutes, and stays bounded forever (vs O(history) pure-drop rebuild that grows with retention).Cadence rationale (why daily, not finer/coarser)
The day partition is the cost quantum. Hourly refreshes rescan today-so-far repeatedly during business hours (recreating the problem, milder); weekly saves nothing per-day-covered and grows the recovery/reconcile tail 7x. Daily anchored to rollover folds exactly one complete settled partition. The rollup has no freshness SLA of its own anymore — its only consumer is disaster recovery, which is minutes-fast at any cadence.
Expected wins
MATERIALIZER_MAX_SNAPSHOT_SPAN=32(dimo-node#123 stopgap) can return to 16.signals_latestchurn drops from ~288 rewrite cycles/day to 1 → the delete-churn fragmentation problem (din#13'srewrite_data_filesmotivation) essentially disappears at the source; din maintenance cycles get cheaper.Out of scope / later
events_latest: same pattern, no KV equivalent yet — untouched here; measure its share and consider the same treatment (or a KV) separately.lake_rollup.gofallback removal make the eventual drop trivial. Not now — the rollup-as-recovery-snapshot keeps KV rebuild O(1 day) instead of O(history).LAKE_REBUILD_ROLLUP_ON_BOOT/RecomputeRollupstay as the deep-DR path (unchanged cost class).Migration order (each step independently shippable)
dq_lake_latest_kv_*coverage metrics and span durations.MATERIALIZER_MAX_SNAPSHOT_SPANback to default; retirelake_rollup.gofallback when confident.