perf(measurements): cover soft-delete reads and cap the all-time live scan

Add a covering partial index over (user_id, type, measured_at DESC) WHERE
deleted_at IS NULL (migration 0183). The hot reads all filter deleted_at and
order measured_at DESC, but the base index is ASC and excludes deleted_at, so
every type not served by the two existing partial indexes pays a heap re-check
of deleted_at over its partition. The covering partial removes the re-check for
the DISTINCT-ON-latest, list and series reads.

Cap the cold-fallback all-time aggregate in summaries-slice at a 15-year
window. It sits far beyond any real history, so it never changes a real
all-time count/min/max/mean; it exists only so a coverage-miss cold read can't
trigger an unbounded full-partition scan.
This commit is contained in:
Marc-André Bombeck committed 2026-06-20 16:52:19 +02:00
1 parent 56d8cf19c5
commit 3fb1f3989c
4 files changed
+72 -8

No files matched your search

@@ -592,15 +592,18 @@ describe("computeSummariesSlice", () => {
// 90-day cap to `canonicalMeasurementsFrom(rank, "90 days")`. The
// helper lives in `@/lib/analytics/source-rank-sql` (stubbed
// above), so pin the contract at the call boundary: the slice
// must ask for the 90-day window. The `allTime` aggregate
// deliberately calls it WITHOUT an interval (no cap — all-time
// count/min/max/mean must scan every row).
// must ask for the 90-day window.
expect(canonicalMeasurementsFrom).toHaveBeenCalledWith(
expect.any(String),
"90 days",
);
// v1.18.10 P-7 — the `allTime` aggregate is now capped at a generous
// 15-year scan-DoS floor (far beyond any real history) so a
// coverage-miss cold read can't trigger an unbounded full-partition
// scan. It no longer calls the helper without an interval.
expect(canonicalMeasurementsFrom).toHaveBeenCalledWith(
expect.any(String),
"15 years",
);
});
});
+20 -5
View File
@@ -81,11 +81,22 @@ import { resolveUserTimezone } from "@/lib/tz/resolver";
const DAY_MS = 24 * 60 * 60 * 1000;
/**
* v1.18.10 P-7 — upper bound on the cold-fallback `allTime` aggregate's
* scan window. Sits far beyond any real account's history (the earliest prod
* measurements are < 10 years old) so it never changes a real all-time
* count / min / max / mean; it exists purely so a coverage-miss cold read
* can't trigger an unbounded full-partition scan. Whitelisted by
* `canonicalMeasurementsFrom`'s interval regex (`<N> years`).
*/
const ALL_TIME_LIVE_CAP_INTERVAL = "15 years" as const;
/**
* v1.4.48 — all-time aggregate row used on the cold-mount fallback
* path. Holds the linearly composable columns (`count / min / max /
* mean`) that must reflect every row the user has ever logged for
* that type, so this query intentionally has no `measured_at` cap.
* that type, capped at `ALL_TIME_LIVE_CAP_INTERVAL` (v1.18.10 P-7) as a
* scan-DoS floor.
*/
interface AllTimeAggregateRow {
type: string;
@@ -608,9 +619,13 @@ async function computeFromRollups(userId: string): Promise<SummariesSlice> {
* v1.4.48 — split into two parallel queries:
*
* 1. `allTime` — the linearly composable columns (`count / min /
* max / mean`) keep the full-partition scan because they must
* reflect every row the user has ever logged. No `measured_at`
* cap.
* max / mean`). v1.18.10 P-7 caps this at `ALL_TIME_LIVE_CAP_INTERVAL`
* (15 years) so an un-windowed cold-fallback read on a coverage-miss
* tenant can never scan an unbounded partition. The cap is a scan-DoS
* floor, not a semantic change: it sits far beyond any real account's
* history (the earliest prod rows are < 10 years old) and the steady
* state is rollup-backed anyway, so it only ever bounds the cold path.
*
*
* 2. `windowed` — `avg7 / avg30` and the slope/r² tuples are all
* already filtered to a 7/30/90 day window inside the SELECT.
@@ -648,7 +663,7 @@ async function computeFromLiveAggregate(
MIN(m."value")::double precision AS min_value,
MAX(m."value")::double precision AS max_value,
AVG(m."value")::double precision AS mean_value
FROM ${canonicalMeasurementsFrom(rankUnqualified)}
FROM ${canonicalMeasurementsFrom(rankUnqualified, ALL_TIME_LIVE_CAP_INTERVAL)}
GROUP BY m."type"
`,
userId,