History query performance: bounded /stats, benchmark-justified indexes, Postgres-tier tests (#141) #176
No reviewers
Labels
No labels
area:ai
area:ci-cd
area:notifications
area:observability
area:public-pages
backlog
bug
duplicate
enhancement
help wanted
invalid
question
type:decision
type:feature
type:infra
type:maintenance
type:security
v1.0.1
v1.1.0
v1.2.0
v1.3.0
v2.0.0
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
rbrooks/WeatherBot!176
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/history-performance"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #141. Closes #174. The milestone's final engineering item — every decision here is measured, not guessed.
/stats bounding
The unbounded whole-history scan the issue called out is gone:
?range=selector (30d/90d/365d/all-retained, default 90d), plain-GET form per convention. "All-retained" is still bounded at the unified horizon — retention has already deleted anything older, so it's the widest window that can return rows, and every variant stays an indexed range scan. Python aggregation kept (the portable-date-bucketing rationale holds for the bounded result sizes; comment updated with measured row counts).Indexes — migration 0036, five created, one measured-and-rejected
Benchmarked via the new
scripts/benchmark_history.py: a seeded ~13-month fixture (60k sent_alerts / 120k snapshots / 600k lightning / 5k events + 20k members) on ephemeral Postgres 16,EXPLAIN (ANALYZE, BUFFERS)over every history query shape, before/after on identical data. Highlights (p95):ix_sent_alerts_dashboard_sent_at(partial,channel_id IS NULL)ix_sent_alerts_location_sent_atix_nws_alert_snapshots_received_atix_notification_deliveries_updated_atix_weather_events_started_atRejected with numbers:
ai_summary_records(created_at)— plan flips but time doesn't move (28.3→28.7 ms); that delete's cost is the cascade intoai_summary_attempts, so the index would be pure write overhead. Kept in the script'sREJECTED_INDEXESfor re-verification. Equal value in what the audit didn't add: five suspect query shapes were already served by existing unique/composite indexes and were left alone.Partitioning verdict: NOT justified — recorded with the numbers
Decision rule from the pickup comment: reads <100 ms p95 and retention deletes <5 s ⇒ plain indexes win. Measured: slowest read 12.4 ms, slowest delete 0.93 s — an order of magnitude inside, stable across three runs. The migration docstring records the verdict, the honesty caveats about seq-scan timing noise, and the scheme to reach for first if scale ever changes (monthly RANGE partitioning of
lightning_clusters— 10× everything else's rows — and explicitly notsent_alerts, whose dedup constraints wouldn't survive).Postgres-tier tests
7 new
@pytest.mark.postgrestests: month aggregation vsdate_truncground truth, local-vs-UTC day bucketing, JSONB operators, real-timestamptzcutoff, and — after a realalembic upgrade headfrom empty — that 0036's indexes exist with the right partial predicate and the rejected index stayed out, plus model↔migration index parity.The #174 "flake" — root cause was the wall clock, not test ordering
The prompt metadata embeds
current_utc_timewith microseconds; when the clock reads second 38 with microseconds in [600000,700000), the timestamp renders…:38.6xxxxxand the test'sassert str(loc.lat) not in prompt(lat 38.6) matched the timestamp — P≈1/600 per run. Proven deterministically by pinning the colliding instant. Fixed at the root: an autouse frozen-clock fixture for the prompt-shape tests, a real contract assertion on the filtered payload, and a regression test that pins the colliding instant.Verification
Dev host, final code: bulk suite 979 passed; Postgres tier 13 passed against ephemeral PG 16 (including the from-empty migration test).
ruffclean. Migration 0036 is index-only — no data changes — so per [migration-real-db-verify] the empty-DB CI run is representative; index builds on prod's row counts are sub-second.🤖 Generated with Claude Code