From 33dbf2e7acac89a7ca008f6e7ed2bc79f40ab525 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 18 Feb 2026 05:47:26 +0000 Subject: [PATCH] perf(server): optimize getPlaylistWithSongs with innerJoin to avoid N+1 queries Replaced the N+1 query pattern (fetching playlist, then fetching song IDs, then fetching songs) with a single efficient SQL query using `innerJoin`. - Reduces database round trips from 3 to 2 (1 for playlist metadata + 1 for songs). - Eliminates manual client-side mapping and filtering. - Ensures stable song order using `orderBy(playlistSongs.id)` (insertion order). - Uses `getTableColumns(songs)` to fetch clean song objects directly. Co-authored-by: Krosebrook <214532761+Krosebrook@users.noreply.github.com> --- .jules/bolt.md | 4 ++++ server/storage.ts | 23 +++++++---------------- 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/.jules/bolt.md b/.jules/bolt.md index 1fe6484..e78a940 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -1,3 +1,7 @@ ## 2026-02-10 - Atomic Database Updates **Learning:** Read-modify-write patterns for counters (like `playCount`) cause race conditions and extra DB round trips. **Action:** Use atomic SQL updates (e.g., `playCount = playCount + 1`) with `returning()` to ensure data integrity and performance. + +## 2026-02-25 - N+1 Query Optimization in Playlists +**Learning:** `getPlaylistWithSongs` was using an N+1 pattern (fetching IDs then fetching songs) which caused multiple DB round trips and required manual sorting. +**Action:** Replaced with a single `innerJoin` query using `getTableColumns` and `orderBy(playlistSongs.id)` to fetch songs in insertion order efficiently. diff --git a/server/storage.ts b/server/storage.ts index c8fc131..f3bfc4c 100644 --- a/server/storage.ts +++ b/server/storage.ts @@ -160,24 +160,15 @@ export class DatabaseStorage implements IStorage { const playlist = await this.getPlaylist(id); if (!playlist) return undefined; - const playlistSongRows = await db.select({ songId: playlistSongs.songId }) - .from(playlistSongs) - .where(eq(playlistSongs.playlistId, id)); - - const songIds = playlistSongRows.map(r => r.songId).filter(id => typeof id === 'number' && !isNaN(id)); - - if (songIds.length === 0) { - return { ...playlist, songs: [] }; - } - - const songsResult = await db.select() + // Optimized: Single query using innerJoin to fetch songs directly in insertion order (by playlistSongs.id) + // This replaces the previous N+1 pattern of fetching IDs then fetching songs + const songsList = await db.select(getTableColumns(songs)) .from(songs) - .where(inArray(songs.id, songIds)); - - const songMap = new Map(songsResult.map(s => [s.id, s])); - const songsList = songIds.map(id => songMap.get(id)).filter((s): s is Song => !!s); + .innerJoin(playlistSongs, eq(songs.id, playlistSongs.songId)) + .where(eq(playlistSongs.playlistId, id)) + .orderBy(playlistSongs.id); - return { ...playlist, songs: songsList }; + return { ...playlist, songs: songsList as Song[] }; } async createPlaylist(insertPlaylist: InsertPlaylist): Promise {