perf(home): aggregate-first rewrites for two scan-the-world queries
handleGetHome itself is well-architected (5 sections in parallel via goroutines, latency-bound by the slowest single query). The cold- start lag is two of those queries doing wider scans than necessary. ListLastPlayedArtistsForUser was iterating FROM artists a with a LATERAL play_events join per row — O(total_artists in library) plan even for users who've only played a handful. Inverted: aggregate the user's plays by artist_id first via the play_events → tracks join (uses play_events_user_track_idx + tracks pkey), then attach the artist row and lateral cover/count subqueries only for the artists that actually appear. Cost now bounded by play history, not library size. ListMostPlayedTracksForUser was joining tracks/albums/artists for every play_event row before grouping — O(total plays) work for joins. Pre-aggregated play_events into a CTE keyed by track_id + count(*), then joined to tracks/albums/artists only for the distinct-tracks survivors. Order-by uses the pre-computed count. No handler or generated-Go signature changes — both queries return the same rowset shape, just much faster on libraries where total artists/plays >> distinct-played-artists/distinct-played-tracks.
This commit is contained in:
@@ -12,17 +12,19 @@ import (
|
||||
)
|
||||
|
||||
const listLastPlayedArtistsForUser = `-- name: ListLastPlayedArtistsForUser :many
|
||||
SELECT a.id, a.name, a.sort_name, a.mbid, a.created_at, a.updated_at, a.artist_thumb_path, a.artist_fanart_path, a.artist_art_source, a.artist_art_sources_version,
|
||||
cov.id AS cover_album_id,
|
||||
cnt.album_count::bigint AS album_count,
|
||||
max_started.started_at::timestamptz AS last_played_at
|
||||
FROM artists a
|
||||
JOIN LATERAL (
|
||||
SELECT max(pe.started_at) AS started_at
|
||||
WITH user_plays AS (
|
||||
SELECT t.artist_id, max(pe.started_at) AS last_started
|
||||
FROM play_events pe
|
||||
JOIN tracks t ON t.id = pe.track_id
|
||||
WHERE pe.user_id = $1 AND t.artist_id = a.id
|
||||
) max_started ON max_started.started_at IS NOT NULL
|
||||
WHERE pe.user_id = $1
|
||||
GROUP BY t.artist_id
|
||||
)
|
||||
SELECT a.id, a.name, a.sort_name, a.mbid, a.created_at, a.updated_at, a.artist_thumb_path, a.artist_fanart_path, a.artist_art_source, a.artist_art_sources_version,
|
||||
cov.id AS cover_album_id,
|
||||
cnt.album_count::bigint AS album_count,
|
||||
up.last_started::timestamptz AS last_played_at
|
||||
FROM user_plays up
|
||||
JOIN artists a ON a.id = up.artist_id
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT id FROM albums
|
||||
WHERE artist_id = a.id AND cover_art_path IS NOT NULL
|
||||
@@ -32,7 +34,7 @@ LEFT JOIN LATERAL (
|
||||
SELECT count(*) AS album_count
|
||||
FROM albums WHERE artist_id = a.id
|
||||
) cnt ON true
|
||||
ORDER BY max_started.started_at DESC, a.id
|
||||
ORDER BY up.last_started DESC, a.id
|
||||
LIMIT $2
|
||||
`
|
||||
|
||||
@@ -52,6 +54,15 @@ type ListLastPlayedArtistsForUserRow struct {
|
||||
// a derived cover_album_id via a representative-album lateral join (most
|
||||
// recent album that has cover_art_path set). album_count joined for the
|
||||
// ArtistRef wire shape.
|
||||
//
|
||||
// Earlier shape iterated `FROM artists a` and ran a LATERAL play_events
|
||||
// subquery per artist — O(total_artists) plan even for users with a
|
||||
// handful of plays. New shape aggregates the user's plays by artist
|
||||
// via the play_events → tracks join up front (uses
|
||||
// play_events_user_track_idx + tracks pkey lookups), then attaches
|
||||
// the artist row and lateral cover/count subqueries only for the
|
||||
// artists that actually appear. Distinct-artists set is small for a
|
||||
// typical user, so cost is bounded by play history not library size.
|
||||
func (q *Queries) ListLastPlayedArtistsForUser(ctx context.Context, arg ListLastPlayedArtistsForUserParams) ([]ListLastPlayedArtistsForUserRow, error) {
|
||||
rows, err := q.db.Query(ctx, listLastPlayedArtistsForUser, arg.UserID, arg.Limit)
|
||||
if err != nil {
|
||||
@@ -87,24 +98,24 @@ func (q *Queries) ListLastPlayedArtistsForUser(ctx context.Context, arg ListLast
|
||||
}
|
||||
|
||||
const listMostPlayedTracksForUser = `-- name: ListMostPlayedTracksForUser :many
|
||||
WITH plays AS (
|
||||
SELECT track_id, count(*) AS cnt
|
||||
FROM play_events
|
||||
WHERE user_id = $1 AND was_skipped = false
|
||||
GROUP BY track_id
|
||||
)
|
||||
SELECT t.id, t.title, t.album_id, t.artist_id, t.track_number, t.disc_number, t.duration_ms, t.file_path, t.file_size, t.file_format, t.bitrate, t.mbid, t.genre, t.added_at, t.updated_at,
|
||||
albums.title AS album_title,
|
||||
artists.name AS artist_name
|
||||
FROM tracks t
|
||||
JOIN albums ON albums.id = t.album_id
|
||||
JOIN artists ON artists.id = t.artist_id
|
||||
JOIN play_events pe ON pe.track_id = t.id
|
||||
WHERE pe.user_id = $1
|
||||
AND pe.was_skipped = false
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM lidarr_quarantine q
|
||||
WHERE q.user_id = $1 AND q.track_id = t.id
|
||||
)
|
||||
GROUP BY t.id, t.title, t.album_id, t.artist_id, t.duration_ms, t.file_path,
|
||||
t.file_format, t.file_size, t.bitrate, t.track_number, t.disc_number,
|
||||
t.mbid, t.genre, t.added_at, t.updated_at,
|
||||
albums.title, artists.name
|
||||
ORDER BY count(*) DESC, t.id
|
||||
FROM plays p
|
||||
JOIN tracks t ON t.id = p.track_id
|
||||
JOIN albums ON albums.id = t.album_id
|
||||
JOIN artists ON artists.id = t.artist_id
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM lidarr_quarantine q
|
||||
WHERE q.user_id = $1 AND q.track_id = t.id
|
||||
)
|
||||
ORDER BY p.cnt DESC, t.id
|
||||
LIMIT $2
|
||||
`
|
||||
|
||||
@@ -122,6 +133,11 @@ type ListMostPlayedTracksForUserRow struct {
|
||||
// M6a: top-N tracks by completed-play count for the user. was_skipped
|
||||
// excludes skips so a user spamming next doesn't fabricate a top track.
|
||||
// Quarantined tracks (per-user soft-hide from M5b) are filtered out.
|
||||
//
|
||||
// Aggregate play_events first (uses play_events_user_track_idx) and
|
||||
// join through tracks/albums/artists only for the survivors. Earlier
|
||||
// shape did the join across every play_event row before grouping —
|
||||
// O(plays) instead of O(distinct tracks).
|
||||
func (q *Queries) ListMostPlayedTracksForUser(ctx context.Context, arg ListMostPlayedTracksForUserParams) ([]ListMostPlayedTracksForUserRow, error) {
|
||||
rows, err := q.db.Query(ctx, listMostPlayedTracksForUser, arg.UserID, arg.Limit)
|
||||
if err != nil {
|
||||
|
||||
@@ -206,24 +206,29 @@ LIMIT $3;
|
||||
-- M6a: top-N tracks by completed-play count for the user. was_skipped
|
||||
-- excludes skips so a user spamming next doesn't fabricate a top track.
|
||||
-- Quarantined tracks (per-user soft-hide from M5b) are filtered out.
|
||||
--
|
||||
-- Aggregate play_events first (uses play_events_user_track_idx) and
|
||||
-- join through tracks/albums/artists only for the survivors. Earlier
|
||||
-- shape did the join across every play_event row before grouping —
|
||||
-- O(plays) instead of O(distinct tracks).
|
||||
WITH plays AS (
|
||||
SELECT track_id, count(*) AS cnt
|
||||
FROM play_events
|
||||
WHERE user_id = $1 AND was_skipped = false
|
||||
GROUP BY track_id
|
||||
)
|
||||
SELECT sqlc.embed(t),
|
||||
albums.title AS album_title,
|
||||
artists.name AS artist_name
|
||||
FROM tracks t
|
||||
JOIN albums ON albums.id = t.album_id
|
||||
JOIN artists ON artists.id = t.artist_id
|
||||
JOIN play_events pe ON pe.track_id = t.id
|
||||
WHERE pe.user_id = $1
|
||||
AND pe.was_skipped = false
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM lidarr_quarantine q
|
||||
WHERE q.user_id = $1 AND q.track_id = t.id
|
||||
)
|
||||
GROUP BY t.id, t.title, t.album_id, t.artist_id, t.duration_ms, t.file_path,
|
||||
t.file_format, t.file_size, t.bitrate, t.track_number, t.disc_number,
|
||||
t.mbid, t.genre, t.added_at, t.updated_at,
|
||||
albums.title, artists.name
|
||||
ORDER BY count(*) DESC, t.id
|
||||
FROM plays p
|
||||
JOIN tracks t ON t.id = p.track_id
|
||||
JOIN albums ON albums.id = t.album_id
|
||||
JOIN artists ON artists.id = t.artist_id
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM lidarr_quarantine q
|
||||
WHERE q.user_id = $1 AND q.track_id = t.id
|
||||
)
|
||||
ORDER BY p.cnt DESC, t.id
|
||||
LIMIT $2;
|
||||
|
||||
-- name: ListLastPlayedArtistsForUser :many
|
||||
@@ -231,17 +236,28 @@ LIMIT $2;
|
||||
-- a derived cover_album_id via a representative-album lateral join (most
|
||||
-- recent album that has cover_art_path set). album_count joined for the
|
||||
-- ArtistRef wire shape.
|
||||
SELECT sqlc.embed(a),
|
||||
cov.id AS cover_album_id,
|
||||
cnt.album_count::bigint AS album_count,
|
||||
max_started.started_at::timestamptz AS last_played_at
|
||||
FROM artists a
|
||||
JOIN LATERAL (
|
||||
SELECT max(pe.started_at) AS started_at
|
||||
--
|
||||
-- Earlier shape iterated `FROM artists a` and ran a LATERAL play_events
|
||||
-- subquery per artist — O(total_artists) plan even for users with a
|
||||
-- handful of plays. New shape aggregates the user's plays by artist
|
||||
-- via the play_events → tracks join up front (uses
|
||||
-- play_events_user_track_idx + tracks pkey lookups), then attaches
|
||||
-- the artist row and lateral cover/count subqueries only for the
|
||||
-- artists that actually appear. Distinct-artists set is small for a
|
||||
-- typical user, so cost is bounded by play history not library size.
|
||||
WITH user_plays AS (
|
||||
SELECT t.artist_id, max(pe.started_at) AS last_started
|
||||
FROM play_events pe
|
||||
JOIN tracks t ON t.id = pe.track_id
|
||||
WHERE pe.user_id = $1 AND t.artist_id = a.id
|
||||
) max_started ON max_started.started_at IS NOT NULL
|
||||
WHERE pe.user_id = $1
|
||||
GROUP BY t.artist_id
|
||||
)
|
||||
SELECT sqlc.embed(a),
|
||||
cov.id AS cover_album_id,
|
||||
cnt.album_count::bigint AS album_count,
|
||||
up.last_started::timestamptz AS last_played_at
|
||||
FROM user_plays up
|
||||
JOIN artists a ON a.id = up.artist_id
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT id FROM albums
|
||||
WHERE artist_id = a.id AND cover_art_path IS NOT NULL
|
||||
@@ -251,7 +267,7 @@ LEFT JOIN LATERAL (
|
||||
SELECT count(*) AS album_count
|
||||
FROM albums WHERE artist_id = a.id
|
||||
) cnt ON true
|
||||
ORDER BY max_started.started_at DESC, a.id
|
||||
ORDER BY up.last_started DESC, a.id
|
||||
LIMIT $2;
|
||||
|
||||
-- name: ListRediscoverAlbumsForUser :many
|
||||
|
||||
Reference in New Issue
Block a user