From cd3867ebda7601a87a7ee250aff5250025f830ba Mon Sep 17 00:00:00 2001 From: Raymond Jacobson Date: Tue, 9 Jun 2026 14:26:30 -0700 Subject: [PATCH] fix(api): stop profile/dashboard track queries from seq-scanning tracks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #932 folded accepted-collaborator tracks into GET /users/:id/tracks (and the dashboard monthly-listens query) as `OR t.track_id IN (SELECT ... FROM track_collaborators ...)`. That OR across two access paths defeats the tracks(owner_id) index, so Postgres sequential-scanned the entire tracks table on every profile load — a severe regression on a top-QPS endpoint. Fix: fetch the user's accepted-collaboration track ids first (a tiny index-only lookup that returns nothing for ~all users), and only widen the query when the set is non-empty — via an explicit `= ANY($ids)` array (bitmap-OR of the owner_id index and the track_id PK), not a correlated subquery. For users with no collaborations the query is byte-identical to the pre-#932 owner-only query and plan. Same treatment for v1_users_listen_counts_monthly. Behavior is unchanged (tests for the merge, pending-hidden, and the no-collab path all pass). Co-Authored-By: Claude Opus 4.8 --- api/v1_users_listen_counts_monthly.go | 36 +++++++++++++++++++---- api/v1_users_tracks.go | 42 +++++++++++++++++++++++---- 2 files changed, 66 insertions(+), 12 deletions(-) diff --git a/api/v1_users_listen_counts_monthly.go b/api/v1_users_listen_counts_monthly.go index e566fcb7..cfe81110 100644 --- a/api/v1_users_listen_counts_monthly.go +++ b/api/v1_users_listen_counts_monthly.go @@ -18,6 +18,27 @@ func (app *ApiServer) v1UsersListenCountsMonthly(c *fiber.Ctx) error { return err } + userId := app.getUserId(c) + + // See v1_users_tracks.go: fold the (usually empty) collaborator set in via an + // explicit id array only when present, so the common case keeps its owner-only + // plan rather than seq-scanning tracks. + collabRows, err := app.pool.Query(c.Context(), + `SELECT track_id FROM track_collaborators WHERE collaborator_user_id = $1 AND status = 'accepted'`, + userId) + if err != nil { + return err + } + collabTrackIds, err := pgx.CollectRows(collabRows, pgx.RowTo[int32]) + if err != nil { + return err + } + + ownerFilter := "owner_id = @userId" + if len(collabTrackIds) > 0 { + ownerFilter = "(owner_id = @userId OR track_id = ANY(@collab_track_ids))" + } + sql := ` SELECT play_item_id, @@ -26,9 +47,7 @@ func (app *ApiServer) v1UsersListenCountsMonthly(c *fiber.Ctx) error { FROM aggregate_monthly_plays WHERE play_item_id IN ( SELECT track_id FROM tracks WHERE stem_of IS NULL - AND (owner_id = @userId - OR track_id IN (SELECT track_id FROM track_collaborators - WHERE collaborator_user_id = @userId AND status = 'accepted')) + AND ` + ownerFilter + ` AND (access_authorities IS NULL OR (COALESCE(@authed_wallet, '') <> '' AND EXISTS (SELECT 1 FROM unnest(access_authorities) aa WHERE lower(aa) = lower(@authed_wallet)))) @@ -39,12 +58,17 @@ func (app *ApiServer) v1UsersListenCountsMonthly(c *fiber.Ctx) error { ; ` - rows, err := app.pool.Query(c.Context(), sql, pgx.NamedArgs{ - "userId": app.getUserId(c), + args := pgx.NamedArgs{ + "userId": userId, "startTime": params.StartTime, "endTime": params.EndTime, "authed_wallet": app.tryGetAuthedWallet(c), - }) + } + if len(collabTrackIds) > 0 { + args["collab_track_ids"] = collabTrackIds + } + + rows, err := app.pool.Query(c.Context(), sql, args) if err != nil { return err } diff --git a/api/v1_users_tracks.go b/api/v1_users_tracks.go index 908b66ef..8955018b 100644 --- a/api/v1_users_tracks.go +++ b/api/v1_users_tracks.go @@ -64,19 +64,46 @@ func (app *ApiServer) v1UserTracks(c *fiber.Ctx) error { gateConditions := queryMulti(c, "gate_condition") gateFilter := buildGateConditionFilter(gateConditions) + // Fetch the user's accepted-collaboration track ids up front. This is a + // small, index-only lookup (covering index on + // track_collaborators(collaborator_user_id, status, track_id)). For the + // overwhelming majority of users it returns nothing, which lets us run the + // original owner-only query with its original plan. Folding the collaborator + // set into the main WHERE as `OR t.track_id IN (subquery)` instead made + // Postgres sequential-scan the entire tracks table on every profile load. + collabRows, err := app.pool.Query(c.Context(), + `SELECT track_id FROM track_collaborators WHERE collaborator_user_id = $1 AND status = 'accepted'`, + userId) + if err != nil { + return err + } + collabTrackIds, err := pgx.CollectRows(collabRows, pgx.RowTo[int32]) + if err != nil { + return err + } + + // Default (no collaborations): identical to the original owner-only query — + // `u` is the profile user, so the artist pick comes from the join. + ownerFilter := "t.owner_id = @user_id" + pinExpr := "t.track_id = u.artist_pick_track_id" + if len(collabTrackIds) > 0 { + // Use an explicit id array (plans far better than a correlated subquery, + // via a bitmap OR of the owner_id index and the track_id PK). `u` may be + // another owner for collab tracks, so the pin references the profile user. + ownerFilter = "(t.owner_id = @user_id OR t.track_id = ANY(@collab_track_ids))" + pinExpr = "t.track_id = (SELECT artist_pick_track_id FROM users WHERE user_id = @user_id)" + } + // The profile lists a user's own tracks plus tracks they've accepted a // collaborator credit on. `u` is the track's owner (the deactivation check - // stays on the owner); the artist-pick pin references the profile user, so a - // collaborator's track is never spuriously pinned by the owner's pick. + // stays on the owner). sql := ` SELECT track_id FROM tracks t JOIN users u ON owner_id = u.user_id LEFT JOIN aggregate_plays ON track_id = play_item_id LEFT JOIN aggregate_track USING (track_id) - WHERE (t.owner_id = @user_id - OR t.track_id IN (SELECT track_id FROM track_collaborators - WHERE collaborator_user_id = @user_id AND status = 'accepted')) + WHERE ` + ownerFilter + ` AND u.is_deactivated = false AND t.is_delete = false AND t.is_available = true @@ -85,7 +112,7 @@ func (app *ApiServer) v1UserTracks(c *fiber.Ctx) error { AND (t.access_authorities IS NULL OR (COALESCE(@authed_wallet, '') <> '' AND EXISTS (SELECT 1 FROM unnest(t.access_authorities) aa WHERE lower(aa) = lower(@authed_wallet))))` + gateFilter + ` - ORDER BY (CASE WHEN t.track_id = (SELECT artist_pick_track_id FROM users WHERE user_id = @user_id) THEN 0 ELSE 1 END), ` + orderClause + ` + ORDER BY (CASE WHEN ` + pinExpr + ` THEN 0 ELSE 1 END), ` + orderClause + ` LIMIT @limit OFFSET @offset ` @@ -95,6 +122,9 @@ func (app *ApiServer) v1UserTracks(c *fiber.Ctx) error { "my_id": myId, "authed_wallet": app.tryGetAuthedWallet(c), } + if len(collabTrackIds) > 0 { + args["collab_track_ids"] = collabTrackIds + } args["limit"] = params.Limit args["offset"] = params.Offset