Skip to content

perf(api): scope session event aggregation to the listed page#6433

Merged
gustavosbarreto merged 1 commit into
masterfrom
fix/session-list-event-aggregation-full-scan
Jun 6, 2026
Merged

perf(api): scope session event aggregation to the listed page#6433
gustavosbarreto merged 1 commit into
masterfrom
fix/session-list-event-aggregation-full-scan

Conversation

@gustavosbarreto
Copy link
Copy Markdown
Member

What

The session list (SessionList) built the event_types and event_seats
columns from derived tables that aggregate the entire session_events
table on every request:

LEFT JOIN (
    SELECT session_id, string_agg(DISTINCT type::text, ',') AS types
    FROM session_events
    GROUP BY session_id          -- no filter: scans the whole table
) event_types ON session.id = event_types.session_id

Because the derived table isn't correlated, Postgres has to scan and
GROUP BY every row in session_events before it can join and apply the
LIMIT. The cost scales with the total number of events, not the
page size, so the query degrades into a full scan (two of them, plus
external-merge sorts that spill to disk) as the table grows.

This replaces the derived tables with correlated subqueries scoped to
each session row, so the aggregation runs only for the sessions on the
page and uses the existing session_events_session_id_created_at_idx
index.

Why

session_events is one of the largest tables in a busy deployment. The
list query was taking several seconds and getting slower the more the
namespace was used, even though every page only returns a handful of
rows. The pagination total + page count made the full scan visible.

Measured (local, synthetic dataset)

EXPLAIN (ANALYZE) of the list query, page of 10:

Dataset Before After
100k sessions / 800k events ~6.1 s ~2.4 ms
100k sessions / 5M events ~11.9 s ~2.9 ms

Before: 2× Seq Scan on session_events + external-merge sorts to disk.
After: index scan on sessions (LIMIT 10) + correlated index lookups.
The "after" time stays flat as the table grows; the "before" grows with it.

How to verify

Open the sessions list on a namespace with a large session_events
table and compare response time, or run EXPLAIN ANALYZE on the
generated query before/after — the Seq Scan on session_events should
be gone.

Note

SessionSelectQuery lost its sessionID parameter (the correlated form
makes it unnecessary). The cloud repo calls this exported helper and is
updated in a paired PR: shellhub-io/cloud.

The session list built event_types and event_seats from derived tables
that ran string_agg over the whole session_events table with no filter
(GROUP BY session_id). Postgres scanned and grouped every event row on
each request even though the list returns a single page, so the cost
scaled with the total event count instead of the page size. On a busy
namespace this becomes a multi-second full scan of session_events.

Replace the derived tables with correlated subqueries scoped to each
session row. They run only for the page's sessions and use the existing
session_events_session_id_created_at_idx index. On a 100k-session,
5M-event dataset the list dropped from ~12s to ~3ms, and the time no
longer grows with the table.
@gustavosbarreto gustavosbarreto requested a review from a team as a code owner June 5, 2026 22:55
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Jun 5, 2026

Claude finished @gustavosbarreto's task in 3m 33s —— View job


Code Review Complete

Reviewed 1 file across code quality, security, testing, Go patterns, and architecture — no issues found. The code looks good as-is.

The change correctly replaces the derived-table LEFT JOINs with correlated subqueries scoped to each session row, eliminating the full-table scan on session_events. The sessionID parameter removal is clean — the correlated form (WHERE session_id = session.id) naturally handles both the list and single-session paths without needing an explicit filter parameter. The cross-repo impact on cloud/ is acknowledged and handled via a paired PR.


To request another review round, comment /review.

@gustavosbarreto gustavosbarreto enabled auto-merge (rebase) June 6, 2026 01:20
@gustavosbarreto gustavosbarreto merged commit 49f6fa2 into master Jun 6, 2026
11 checks passed
@gustavosbarreto gustavosbarreto deleted the fix/session-list-event-aggregation-full-scan branch June 6, 2026 01:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants