Skip to content

[Perf] MATERIALIZE my_follows CTE in GetTracks/GetPlaylists#792

Merged
raymondjacobson merged 1 commit intomainfrom
ray/perf-materialize-my-follows
May 8, 2026
Merged

[Perf] MATERIALIZE my_follows CTE in GetTracks/GetPlaylists#792
raymondjacobson merged 1 commit intomainfrom
ray/perf-materialize-my-follows

Conversation

@raymondjacobson
Copy link
Copy Markdown
Member

Summary

  • Add MATERIALIZED to the my_follows CTE in get_tracks.sql and get_playlists.sql. Without it, Postgres inlines the CTE and re-runs the follows JOIN aggregate_user + ORDER BY follower_count once per SubPlan invocation in the followee_reposts / followee_favorites projections.
  • Add a regression test covering has_current_user_*, followee_reposts, and followee_favorites (no prior coverage).

Why

GetTracks is the most-called query in the API by total time — 268M calls / 3.1B ms in pg_stat_statements. It runs once per request that returns track data (trending, feed, search results, my-favorites, …), and it's where personalized fields are computed.

Impact

Verified on the prod read replica with user 20 (1752 follows) and a 10-track id list:

Before After Δ
Execution time 368 ms 80 ms −78%
Shared buffer hits 86,213 45,298 −47%

GetPlaylists shares the same CTE and gets the same fix.

Risk

  • Low. MATERIALIZED is a planner directive — query results are unchanged. Existing test suite passes (go test -count=1 ./api/...), and the new TestGetTrackPersonalization confirms the personalization fields populate correctly with ?user_id=.
  • The CTE is now allocated once per query (a few hundred rows); the trade-off is positive in every realistic case for users with >1 followee.

Test plan

  • go test -count=1 ./api/... (full suite, all green)
  • EXPLAIN ANALYZE on read replica confirms 4.6× speedup on representative track-id sets
  • Local server hits /v1/tracks/trending?user_id=Wem1e (Phuture, 1752 follows) — 500ms warm vs 480ms unauth, basically free personalization
  • New regression test TestGetTrackPersonalization exercises both me-perspective flags and followee-perspective arrays

🤖 Generated with Claude Code

Without MATERIALIZED, Postgres inlines the CTE and re-runs the
follows JOIN aggregate_user + ORDER BY follower_count once per
SubPlan invocation in the followee_reposts/followee_favorites
projections. For users with many follows this dominates the query.

Verified on the production read replica with user 20 (1752 follows)
and a 10-track id list:

  Before: 368 ms exec, 86,213 shared buffer hits
  After:   80 ms exec, 45,298 shared buffer hits  (~4.6x)

GetTracks runs ~268M times in pg_stat_statements (the most-called
personalization query in the API), so the savings compound.

Adds a regression test covering has_current_user_*, followee_reposts,
and followee_favorites since none existed before.
raymondjacobson added a commit that referenced this pull request May 8, 2026
## Summary

Lower `LIMIT 5000` to `LIMIT 200` in the `my_follows` CTE used by
`GetTracks` and `GetPlaylists`. The CTE feeds two consumers
(`followee_reposts`, `followee_favorites`) that each emit at most 3 rows
ordered by `follower_count DESC`, so 5000 was almost always
materializing more than the LATERAL ever consumed.

## Impact

Verified on the prod read replica with user 20 (1752 follows), 10-track
id list, three warm runs each (with #792 / `MATERIALIZED` applied):

| LIMIT | runs (ms) | mean |
|---|---|---|
| 5000 | 86, 97, 92 | 92 ms |
| 200 | 43, 31, 23 | **32 ms** (~2.9×) |

Stacks with #792. `GetTracks` and `GetPlaylists` together represent
~310M calls / 4.5B ms in `pg_stat_statements`.

## Risk

- A user whose only follower-of-X reposter is ranked >200 by
`follower_count` will no longer surface in `followee_reposts` /
`followee_favorites`. Acceptable trade-off — those low-fanout reposts
are already dominated by the top-200 in the rendered top-3 social proof.
- No correctness change for the >99% of users with <200 follows.
Existing `TestGetTrackPersonalization` (added in #792) and the rest of
the suite cover the personalization shape.

## Test plan

- [x] `go test -count=1 ./api/...` (full suite, all green)
- [x] EXPLAIN ANALYZE on read replica shows ~2.9× warm-cache speedup
- [x] Local server hits `/v1/tracks/trending?user_id=Wem1e` (Phuture,
1752 follows) — 400-600ms warm

## Stacks on

- #792

🤖 Generated with [Claude Code](https://claude.com/claude-code)
@raymondjacobson raymondjacobson merged commit a5cf591 into main May 8, 2026
5 checks passed
@raymondjacobson raymondjacobson deleted the ray/perf-materialize-my-follows branch May 8, 2026 01: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.

1 participant