From b4e763be44d78af2d69deae7386a07ce72f2360a Mon Sep 17 00:00:00 2001 From: Alexander Date: Sun, 10 May 2026 00:03:29 +0200 Subject: [PATCH] perf: fix N+1 query in GetAllByArtistID with batch loading MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- internal/repository/postgres/album.go | 84 +++++++++++++++++++++++++-- 1 file changed, 80 insertions(+), 4 deletions(-) diff --git a/internal/repository/postgres/album.go b/internal/repository/postgres/album.go index a3f7225..bfbdbc9 100644 --- a/internal/repository/postgres/album.go +++ b/internal/repository/postgres/album.go @@ -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 {