Skip to content

Speed up /coaches: partial index and database-side coach count - #2900

Merged
mroderick merged 1 commit into
masterfrom
perf/coach-count-index
Sep 17, 2026
Merged

mroderick merged 1 commit into
masterfrom
perf/coach-count-index

Conversation

@mroderick

Copy link
Copy Markdown
Collaborator

Closes #2886

Summary

The /coaches page counted all-time attended coaches by loading the grouped result into Ruby (Relation#length materialises all 3,572 grouped rows) and aggregating over all 1.98M workshop_invitations rows — the slowest of the page's five queries. This PR:

  • adds a partial index on workshop_invitations(member_id) WHERE attended AND role = 'Coach' (CREATE INDEX CONCURRENTLY), making the aggregation an index-only scan over the 18k matching rows, and
  • replaces top_coach_query.length with COUNT(DISTINCT member_id) so the count never leaves the database as rows.

Measured (production dump, in-process, warm medians)

before after
page db_runtime 112ms 46ms
all-time coach aggregation 78.6ms (parallel seq scan) 3.3ms (index-only scan)
count query 1.6ms

Production context: post-#2889 the page sits at ~320-375ms total, db 252-344ms; this addresses most of what remains.

Trade-off

The index predicate must keep matching the to_coaches/attended scopes; if either scope changes, the index silently stops covering the count. A staging EXPLAIN after deploy is recommended. The index adds a small write cost to coach-attended rows (negligible at this table's write rate).

Verification

  • spec/features/listing_coaches_spec.rb: new scenario pins the header count (same coach at two workshops counts once); 4/4 green.
  • Migration round-trips on the test DB (db:rollback/db:migrate), including the concurrent drop.
  • EXPLAIN on codebar_production_dump confirms the index-only scan.
  • ce-code-review (5 reviewers): verdict "Ready to merge"; the single finding (concurrent rollback drop) applied in the second commit.

The /coaches page counted all-time attended coaches by loading the
grouped result into Ruby (Relation#length) and aggregating over all
1.98M workshop_invitations rows. A partial index on member_id for
attended coach rows makes the aggregation an index-only scan, and the
count now runs as COUNT(DISTINCT member_id) instead of materialising
3,572 rows. The index is created and dropped concurrently. Measured
against the production dump: page db_runtime 112ms to 46ms.

Closes #2886
@mroderick
mroderick force-pushed the perf/coach-count-index branch from cd4cb9f to d052862 Compare September 17, 2026 10:00
@mroderick
mroderick marked this pull request as ready for review September 17, 2026 10:03
@mroderick
mroderick merged commit e7d2ea9 into master Sep 17, 2026
10 checks passed
@mroderick
mroderick deleted the perf/coach-count-index branch September 17, 2026 10:03
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.

/coaches is slow: two unindexed group/count aggregations per request

1 participant