Skip to main content
← Back to list
01Issue
FeatureOpenSwamp Club
AssigneesNone

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 /leaderboard render fires 6 ClickHouse queries, totalling ~2.36M rows read and ~660 MiB peak memory. Four of them run concurrently via Promise.all in lib/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:

  1. score_daily (via ownedGrants)
  2. swamp.cli_daily
  3. swamp.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 in cli_daily, only 8 are absent from days_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 the days_active + cli_daily day 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 /leaderboard investigation. The other two cover the shared circuit breaker and two cheaper scan pushdowns.
02Bog Flow
OPENTRIAGEDIN PROGRESSSHIPPED

Open

8/2/2026, 2:12:05 AM

No activity in this phase yet.

03Sludge Pulse
Editable. Press Enter to edit.

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).

Sign in to post a ripple.