perf: fix N+1 query in GetAllByArtistID with batch loading

Replace per-album loadRelations calls with batch queries using ANY($1).
Reduces 1+2N queries to 3 queries total (1.45s → 50ms for 83 albums).
This commit is contained in:
Alexander
2026-05-10 00:03:29 +02:00
parent 88372c9301
commit b4e763be44
+80 -4
View File
@@ -122,18 +122,94 @@ func (r *AlbumRepository) GetAllByArtistID(ctx context.Context, artistID string)
defer rows.Close() defer rows.Close()
var albums []domain.Album var albums []domain.Album
var albumIDs []string
albumIndex := make(map[string]int)
for rows.Next() { for rows.Next() {
album, err := r.scanAlbumFromRow(rows) album, err := r.scanAlbumFromRow(rows)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if err := r.loadRelations(ctx, album); err != nil { albumIndex[album.ID] = len(albums)
return nil, err albumIDs = append(albumIDs, album.ID)
}
albums = append(albums, *album) albums = append(albums, *album)
} }
if err := rows.Err(); err != nil {
return nil, err
}
return albums, rows.Err() if len(albums) == 0 {
return albums, nil
}
if err := r.loadRelationsBatch(ctx, albums, albumIDs, albumIndex); err != nil {
return nil, err
}
return albums, nil
}
func (r *AlbumRepository) loadRelationsBatch(ctx context.Context, albums []domain.Album, albumIDs []string, albumIndex map[string]int) error {
extQuery := `SELECT album_id, source, source_id, url FROM album_external_ids WHERE album_id = ANY($1)`
extRows, err := r.pool.Query(ctx, extQuery, albumIDs)
if err != nil {
return err
}
defer extRows.Close()
for extRows.Next() {
var albumID string
var ext domain.ExternalID
var url *string
if err := extRows.Scan(&albumID, &ext.Source, &ext.SourceID, &url); err != nil {
return err
}
ext.URL = derefString(url)
if idx, ok := albumIndex[albumID]; ok {
albums[idx].ExternalIDs = append(albums[idx].ExternalIDs, ext)
}
}
if err := extRows.Err(); err != nil {
return err
}
artistQuery := `
SELECT aa.album_id, a.id, a.name, a.sort_name, a.artist_type, a.country,
aa.role, aa.position
FROM album_artists aa
JOIN artists a ON a.id = aa.artist_id
WHERE aa.album_id = ANY($1)
ORDER BY aa.album_id, aa.position`
artistRows, err := r.pool.Query(ctx, artistQuery, albumIDs)
if err != nil {
return err
}
defer artistRows.Close()
for artistRows.Next() {
var (
albumID string
ac domain.ArtistCredit
sortName *string
artType *string
country *string
)
if err := artistRows.Scan(
&albumID, &ac.Artist.ID, &ac.Artist.Name, &sortName, &artType, &country,
&ac.Role, &ac.Position,
); err != nil {
return err
}
ac.Artist.SortName = derefString(sortName)
ac.Artist.Type = derefString(artType)
ac.Artist.Country = derefString(country)
if idx, ok := albumIndex[albumID]; ok {
albums[idx].Artists = append(albums[idx].Artists, ac)
}
}
return artistRows.Err()
} }
func (r *AlbumRepository) SaveAll(ctx context.Context, albums []domain.Album) error { func (r *AlbumRepository) SaveAll(ctx context.Context, albums []domain.Album) error {