Skip to content

Admin stats materialized views are full recomputes and evict the OLTP working set #189

Description

@Yuyz0112

Problem

The admin dashboard materialized views defined in control-plane/migrations/001_init.sql
are refreshed on a cron by scheduler/src/admin-stats-refresh.ts. Every refresh is a
full recompute from scratch — there is no incremental path.

Three of them each embed the same subquery independently:

SELECT message_id, count(*) FROM session_events GROUP BY message_id

So a single refresh cycle aggregates the entire session_events table three times.
session_events is typically the largest table in the schema and grows without bound.

admin_daily_stats also computes active_workspaces as a range join of
generate_series(first_workspace_date, CURRENT_DATE, '1 day') against sessions
on a 7-day last_active_at window — cost grows with days × sessions, i.e. close to
quadratic as the deployment ages.

Impact

Observed on a long-running deployment:

view buffer hit ratio temp files written per refresh
admin_daily_stats 65% tens of MB
admin_workspace_stats 43% tens of MB
user_daily_interactions 59% ~10 MB
admin_token_daily_stats 28%

Each of the heavy views takes seconds to rebuild; a full cycle runs well over ten
seconds. The outputs are tiny — a few hundred rows, tens of kB each.

The wall-clock cost is not the real problem. Each pass reads far more than
shared_buffers holds, so it evicts the OLTP working set — ordinary queries go back
to disk right after every refresh. This showed up as periodic latency spikes across
the whole database, not just on the admin dashboard. The low buffer hit ratios in the
table above are the same effect seen from the refresh side.

Proposed direction

  1. Maintain a shared message → event-count table incrementally (trigger on
    session_events, or a watermark-driven rollup). Removes three full aggregations
    of the largest table per cycle.
  2. Roll up by day. Closed days never change, so recompute only today plus a short
    lookback for late arrivals.
  3. Rewrite active_workspaces as a 7-day window over a per-day distinct-workspace
    rollup instead of the generate_series × sessions range join.
  4. admin_workspace_stats has no date dimension (it groups all messages by
    workspace_id) and needs its own incremental design.
  5. admin_token_daily_stats scans workspace_usage_events and gets the same
    per-day treatment.

Workaround in place

Lowering the refresh cadence (ADMIN_STATS_REFRESH_CRON, default */10 * * * *)
reduces the eviction duty cycle but does not bound the cost, which keeps growing
with the data.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions