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:
@@ -122,18 +122,94 @@ func (r *AlbumRepository) GetAllByArtistID(ctx context.Context, artistID string)
|
||||
defer rows.Close()
|
||||
|
||||
var albums []domain.Album
|
||||
var albumIDs []string
|
||||
albumIndex := make(map[string]int)
|
||||
|
||||
for rows.Next() {
|
||||
album, err := r.scanAlbumFromRow(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := r.loadRelations(ctx, album); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
albumIndex[album.ID] = len(albums)
|
||||
albumIDs = append(albumIDs, album.ID)
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user