Relationships
#1495 Materialize an active-days-by-id table for the streak board
Opened by keeb · 8/2/2026
Problem
The streak board is the single most expensive query behind /leaderboard.
Measured 2026-08-01 against local ClickHouse system.query_log (dataset
17.35M events; prod swamp.events is 74.75M rows / 6.72 GiB):
- One
/leaderboardrender fires 6 ClickHouse queries, totalling ~2.36M rows read and ~660 MiB peak memory. Four of them run concurrently viaPromise.allinlib/app/leaderboard-view.ts:100-105. - The streak board alone accounts for 940k rows read / 182 MiB peak, versus ~366k rows / ~150 MiB for a score board.
- A 12-concurrent-viewer test produced 48 queries, 7.66 GiB summed memory, and page latency of 0.12s -> 0.99s.
All of this runs under a 5s max_execution_time
(lib/infrastructure/clickhouse/clickhouse-score-reads.ts:462,
SCORE_READ_CEILING_S) with a 7s client abort, behind a 3-strike /
30s-cooldown breaker — so the headroom between "slow" and "board goes
unavailable" is thin.
Cause
ownedDays in
lib/infrastructure/clickhouse/clickhouse-score-reads.ts:246-280 unions
three day sources:
score_daily(viaownedGrants)swamp.cli_dailyswamp.days_active
streaksSql (same file, :291-310) then does
groupUniqArray(toInt32(day)) per owner for the gaps-and-islands streak
computation. The triple union is what makes it the heaviest read on the
page.
Measured overlap — the union is buying almost nothing
swamp.days_active is a 99.96% superset of the other two:
- Of 30,757 distinct
(distinct_id, day)pairs incli_daily, only 8 are absent fromdays_active. - Of 197k pairs in
score_daily, only 104 are absent.
So the three-way union triples the scan in order to recover 112 pairs.
Proposal
Add a compact table:
swamp.active_days_by_id (distinct_id String, day Date)
ENGINE = ReplacingMergeTree
ORDER BY (distinct_id, day)Fed by materialized views:
- from
swamp.events— covers thedays_active+cli_dailyday sets - from
swamp.score_grants— covers the grant-only days
ownedDays then becomes one ~257k-row keyed scan plus the existing
identity_map join, instead of 940k rows across three sources.
Constraint that must hold
Identity stays late-bound. Key the table by distinct_id, never by
username/owner. Resolution to owner happens at read time via
identity_map, exactly as today — an operative is many machines, and
freezing the owner into the rollup would re-break that.
An MV is not a cache: it fires on INSERT, so the streak board stays realtime. No freshness regression.
Notes
- Prod ClickHouse is an external 3-node replicated cluster and the MVs are
local, so the DDL has to reach all three nodes — same shape as prior
schema changes, see
infrastructure/clickhouse/CLAUDE.md. - This is one of three issues from the same 2026-08-01
/leaderboardinvestigation. The other two cover the shared circuit breaker and two cheaper scan pushdowns.
Closed
No activity in this phase yet.
keeb commented 8/2/2026, 2:12:29 AM
Siblings from the same 2026-08-01 /leaderboard investigation: #1496 (shared score-read circuit breaker) and #1497 (ghost-gate dictionary + eventCounts pushdown).
keeb commented 8/10/2026, 7:21:48 PM
Cross-link: this is now part of epic #1572 (leaderboard + profile at 10B events).
Two notes from the 2026-08-10 measurements that bear on this issue:
The baseline moved. This issue cites prod
swamp.eventsat 74.75M rows (2026-08-01). Nine days later it is 225,037,546 rows / 18.19 GiB — 3x, in nine days. The analysis here is still correct; the urgency is higher than it reads.Still worth doing, and it composes. Materializing active-days attacks the streak board's scan cost, which remains real on a cache miss. #1575 proposes an owner-grain
board_totalsmaintained by the projector, andactive_days_by_idis a natural input to it rather than a competitor. #1575 flags an open question the two should settle together: whetherboard_totalscarries a streak column computed from this table (one read serves all four boards), or the streak board reads this table directly (keeps gaps-and-islands logic in one place).
The late-bound-identity constraint stated here — key by distinct_id, resolve to owner at read time — is preserved in #1575, which is explicit that its owner-keyed projection is a derived rebuild, never a system of record.
swamp_lord commented 8/24/2026, 9:06:30 PM
Rollout plan
Create and backfill the new active-day projection in one additive production rollout before swamp-club reads it.
- Add
swamp.active_days_by_id, keyed by(distinct_id, day), usingReplicatedReplacingMergeTree, monthly partitions, and no owner field so identity remains late-bound. - Add two
ON CLUSTER clickhouse_productionmaterialized views:swamp.events->toDate(created_at)andswamp.score_grants->toDate(granted_at). - Synchronize the durable source replicas on the DDL host, then backfill in the same SQL execution from
days_active UNION cli_daily UNION score_grants, grouped by(distinct_id, day).cli_dailyremains in the historical seed because the investigation measured eight CLI day pairs absent fromdays_active;score_grantscovers grant-only days and the unprojected live edge. - Make retries and live-MV/backfill overlap harmless through set semantics:
ReplacingMergeTreeat the raw key and downstream owner/day deduplication. No ingestion pause or T0 cutoff is required. - Add fail-fast source-minus-target parity assertions and write one idempotent
schema_migrationsrow only after they pass. Mirror the table and both MVs in giga-swamp’sschema.sql. - Apply the one SQL file through
clickhouse-production-apply-sql: all-replica preflight, one full backup, manual approval, one-host apply, then per-replica verification of table/MV definitions, replication health, migration-row uniqueness, and membership parity. - Roll back by dropping the two MVs first and leaving the unused target table in place. Do not restore the full-cluster backup for an additive-object failure.
- Gate the later swamp-club reader change on owner-level parity. The current reader also has stamped grant-label fallback and owner-level badge folding; a two-column raw set must either prove those cases unchanged or document an explicit behavior decision before the reader deploys.
Implementation branch: infra/clickhouse-active-days-by-id in giga-swamp. The production schema/backfill rollout is deliberately separate from the swamp-club reader deployment.
swamp_lord commented 8/24/2026, 9:43:56 PM
Production rollout completed on 2026-08-24. Branch infra/clickhouse-active-days-by-id, commit 29ff13d. clickhouse-production-apply-sql completed after standalone backup premig-active-days-by-id-2026-08-24-r2 reported BACKUP_CREATED. Migration installed swamp.active_days_by_id plus the events and score_grants MVs, backfilled the durable union, and passed source-minus-target assertions. Post-apply verification on ch-01/ch-02/ch-03: both MVs present, 650657 physical rows / 650611 distinct device-day memberships on each replica, and 0 unfinished mutations. Reader switch remains separately gated on owner-level parity and badge/label fallback behavior.
swamp_lord commented 8/24/2026, 11:59:46 PM
Completed by swamp-club PR #1136. The reader now uses swamp.active_days_by_id while preserving late-bound identity resolution, stamped grant-label fallback, ghost visibility gating, collective exclusion, and founder exclusion. Adversarial live ClickHouse parity passed, CI and UAT passed, merge commit a30e9284 deployed successfully on 2026-08-24, and the production streak endpoint is serving results after deployment.
Sign in to post a ripple.