Skip to content

perf(server): drop three unused indexes on the sessions table - #6884

Merged
otavio merged 1 commit into
masterfrom
perf/drop-unused-session-indexes
Aug 12, 2026
Merged

perf(server): drop three unused indexes on the sessions table#6884
otavio merged 1 commit into
masterfrom
perf/drop-unused-session-indexes

Conversation

@otavio

@otavio otavio commented Aug 11, 2026

Copy link
Copy Markdown
Member

Migration 017 drops three indexes on sessions that back no filter, no sort and no constraint,
while costing an index insert on every session row written. Over 66 days of production counters
they served 2, 0 and 0 scans, against 204,400 on sessions_started_at_idx.

Index Scans / 66 d Size
sessions_username_idx 2 8 MB
sessions_type_idx 0 8 MB
sessions_closed_started_idx 0 42 MB

Unlike the devices pair in #6883 there is no read-path trade to weigh here: nothing queries
these columns, so nothing gets slower. Dropping an index reclaims its space immediately, so no
repack is needed either.

username and type are unreachable, not merely unused

Verified against the source in both shellhub and cloud rather than trusted from counters:

  • The session list accepts exactly three filter fields — device_uid, closed, active
    (SessionFilterFields) — and rejects anything else at the route before it reaches SQL.
  • The sort is not user-selectable at all: ListSessions hardcodes
    Sorter{By: "started_at", Order: desc, Tiebreak: "id"}. There is no SessionSortFields.
  • cloud adds no session filters or sorts of its own.
  • No WHERE/ORDER BY on either column exists in either repo. The type = ? predicates that do
    exist are all on session_events, a different table, already served by its session_id/seat
    indexes.

sessions_closed_started_idx is redundant, which is why 0 scans needed explaining

This one does have a consumer, so the counter alone would have been misleading. The
recording-conversion worker runs
WHERE recorded AND closed AND converted = ? ORDER BY started_at DESC LIMIT 10 — close to a
textbook match for (closed, started_at). The query runs on every cron tick; the index is simply
never chosen.

The reason is that its leading column is a near-constant: a session is closed for all but the
minutes it is live, so (closed, started_at) is a strictly fatter duplicate of
sessions_started_at_idx — 42 MB against 26 MB — offering the planner nothing extra for either
the filter or the ORDER BY. The smaller index wins on cost every time, which is exactly what the
204,400-vs-0 split shows.

If that worker ever shows up as a cost, the index it wants is a partial one over the backlog
alone, not this:

CREATE INDEX sessions_pending_conversion_idx ON sessions (started_at DESC)
  WHERE recorded AND closed AND NOT converted;

A few pages instead of 42 MB. Not included here — that would be speculation.

sessions_namespace_id_idx stays

Despite only 63 scans. Deleting a namespace cascades to sessions, and without this index that is
a sequential scan over a 1.19 M-row table. The cascade argument does not depend on idx_scan at
all, so it holds regardless of how shellhub-io/team#200 resolves.

Testing

Full pg store suite green against a schema built from 001 through 017, and verified
end-to-end through bun's runner on the dev stack: 017 applies at boot and leaves sessions with
sessions_pkey, sessions_namespace_id_idx, sessions_device_id_idx and
sessions_started_at_idx.

Fixes shellhub-io/team#199.

@otavio

otavio commented Aug 11, 2026

Copy link
Copy Markdown
Member Author

Follow-up #6886 is stacked on this branch — it enables pg_stat_statements, pgstattuple and pg_buffercache as migration 021. Merge order: #6883#6884#6886.

@gustavosbarreto
gustavosbarreto force-pushed the perf/device-heartbeat-hot-updates branch from ac11f9c to 8a7cda8 Compare August 12, 2026 13:42
@gustavosbarreto
gustavosbarreto force-pushed the perf/drop-unused-session-indexes branch from f9be886 to 84ff059 Compare August 12, 2026 13:42
sessions_username_idx, sessions_type_idx and sessions_closed_started_idx back no filter, no sort
and no constraint, while costing an index insert on every session row written. Over 66 days of
production counters they served 2, 0 and 0 scans, against 204,400 on sessions_started_at_idx.

username and type are unreachable by construction rather than merely unused: the session list
accepts exactly three filter fields (device_uid, closed, active) and rejects anything else at the
route, its sort is hardcoded to started_at with no user-selectable alternative, and cloud adds no
session filters of its own. The type predicates that do exist are all on session_events, a
different table, already served by its session_id and seat indexes.

sessions_closed_started_idx is redundant rather than unused, which is why its 0 scans needed
explaining before trusting them: the recording-conversion worker runs WHERE closed AND recorded
AND NOT converted ORDER BY started_at DESC, which looks tailor-made for (closed, started_at). But
closed is a near-constant — a session is closed for all but the minutes it is live — so the index
is a strictly fatter duplicate of sessions_started_at_idx, 42 MB against 26 MB, and loses to it on
cost for every shape that exists. If that worker ever shows up as a cost, what it wants is a
partial index over the unconverted backlog alone.

sessions_namespace_id_idx stays despite its 63 scans: deleting a namespace cascades to sessions,
and without it that is a sequential scan over 1.19 M rows.

There is no read-path trade here, so nothing gets slower.

Fixes: shellhub-io/team#199
@otavio
otavio force-pushed the perf/drop-unused-session-indexes branch from 84ff059 to c12baa5 Compare August 12, 2026 21:39
@otavio
otavio requested a review from a team as a code owner August 12, 2026 21:39
@otavio
otavio changed the base branch from perf/device-heartbeat-hot-updates to master August 12, 2026 21:39
@otavio
otavio dismissed gustavosbarreto’s stale review August 12, 2026 21:39

The base branch was changed.

@gustavosbarreto

Copy link
Copy Markdown
Member

/review

@otavio
otavio merged commit 1bea1c5 into master Aug 12, 2026
28 of 29 checks passed
@otavio
otavio deleted the perf/drop-unused-session-indexes branch August 12, 2026 21:39
@github-actions

github-actions Bot commented Aug 12, 2026

Copy link
Copy Markdown

Claude finished @gustavosbarreto's task in 15s —— View job


Code Review

This PR is already merged, so I'm skipping the automated review pass — inline feedback on a merged diff can't be acted on in place.

If you'd like a retrospective look at this change (or a review of a follow-up), open a new PR or point me at one and comment /review there.

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