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()
|
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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user