From 691c52f6102f97841168a4ca2c13e3187673245e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fahri=20Can=20Se=C3=A7er?= Date: Sun, 26 Apr 2026 17:07:19 +0300 Subject: [PATCH] perf: replace Prisma relation queries with raw SQL for getExistingMatchIds and getMissingScopes - fixes Pi hang --- .../feeder/feeder-persistence.service.ts | 124 ++++++++---------- 1 file changed, 58 insertions(+), 66 deletions(-) diff --git a/src/modules/feeder/feeder-persistence.service.ts b/src/modules/feeder/feeder-persistence.service.ts index 204d0b7..29697b6 100755 --- a/src/modules/feeder/feeder-persistence.service.ts +++ b/src/modules/feeder/feeder-persistence.service.ts @@ -853,49 +853,34 @@ export class FeederPersistenceService { } async getExistingMatchIds(matchIds: string[]): Promise { - const matches = await this.prisma.match.findMany({ - where: { - id: { in: matchIds }, - oddCategories: { some: {} }, - OR: [ - { - sport: "football", - footballTeamStats: { some: {} }, - playerParticipations: { some: { isStarting: true } }, - }, - { - sport: "basketball", - basketballTeamStats: { some: {} }, - basketballPlayerStats: { some: {} }, - }, - ], - }, - select: { id: true, sport: true }, - }); + if (matchIds.length === 0) return []; - const footballIds = matches - .filter((m) => m.sport === "football") - .map((m) => m.id); - const completeFootballIds = new Set(); + // Use raw SQL for performance — Prisma's { some: {} } relation filters + // generate heavy correlated subqueries that hang on Raspberry Pi with + // large tables (15M+ odd_selections, 3M+ participations). + const result = await this.prisma.$queryRawUnsafe< + Array<{ id: string }> + >( + ` + SELECT m.id + FROM matches m + WHERE m.id = ANY($1::text[]) + AND EXISTS (SELECT 1 FROM odd_categories oc WHERE oc.match_id = m.id) + AND ( + (m.sport = 'football' + AND EXISTS (SELECT 1 FROM football_team_stats fts WHERE fts.match_id = m.id) + AND (SELECT count(*) FROM match_player_participation mpp + WHERE mpp.match_id = m.id AND mpp.is_starting = true) >= 18) + OR + (m.sport = 'basketball' + AND EXISTS (SELECT 1 FROM basketball_team_stats bts WHERE bts.match_id = m.id) + AND EXISTS (SELECT 1 FROM basketball_player_stats bps WHERE bps.match_id = m.id)) + ) + `, + matchIds, + ); - if (footballIds.length > 0) { - const starterCounts = await this.prisma.matchPlayerParticipation.groupBy({ - by: ["matchId"], - where: { - matchId: { in: footballIds }, - isStarting: true, - }, - _count: { _all: true }, - }); - - for (const row of starterCounts) { - if (row._count._all >= 18) completeFootballIds.add(row.matchId); - } - } - - return matches - .filter((m) => m.sport !== "football" || completeFootballIds.has(m.id)) - .map((m) => m.id); + return result.map((r) => r.id); } /** @@ -909,38 +894,45 @@ export class FeederPersistenceService { const result = new Map(); if (matchIds.length === 0) return result; - const matches = await this.prisma.match.findMany({ - where: { - id: { in: matchIds }, - state: "Ended", - }, - select: { - id: true, - sport: true, - _count: { - select: { - playerParticipations: true, - footballTeamStats: true, - basketballTeamStats: true, - basketballPlayerStats: true, - oddCategories: true, - }, - }, - }, - }); + // Use raw SQL for performance on Raspberry Pi. + // Note: state is 'postGame' in DB, not 'Ended'. + const rows = await this.prisma.$queryRawUnsafe< + Array<{ + id: string; + sport: string; + fts_count: bigint; + pp_count: bigint; + bts_count: bigint; + bps_count: bigint; + oc_count: bigint; + }> + >( + ` + SELECT m.id, m.sport::text, + (SELECT count(*) FROM football_team_stats fts WHERE fts.match_id = m.id) as fts_count, + (SELECT count(*) FROM match_player_participation mpp WHERE mpp.match_id = m.id) as pp_count, + (SELECT count(*) FROM basketball_team_stats bts WHERE bts.match_id = m.id) as bts_count, + (SELECT count(*) FROM basketball_player_stats bps WHERE bps.match_id = m.id) as bps_count, + (SELECT count(*) FROM odd_categories oc WHERE oc.match_id = m.id) as oc_count + FROM matches m + WHERE m.id = ANY($1::text[]) + AND m.state = 'postGame' + `, + matchIds, + ); - for (const m of matches) { + for (const m of rows) { const missing: string[] = []; if (m.sport === "football") { - if (m._count.footballTeamStats === 0) missing.push("stats"); - if (m._count.playerParticipations < 18) missing.push("lineups"); + if (Number(m.fts_count) === 0) missing.push("stats"); + if (Number(m.pp_count) < 18) missing.push("lineups"); } else if (m.sport === "basketball") { - if (m._count.basketballTeamStats === 0) missing.push("stats"); - if (m._count.basketballPlayerStats === 0) missing.push("lineups"); + if (Number(m.bts_count) === 0) missing.push("stats"); + if (Number(m.bps_count) === 0) missing.push("lineups"); } - if (m._count.oddCategories === 0) missing.push("odds"); + if (Number(m.oc_count) === 0) missing.push("odds"); if (missing.length > 0) { result.set(m.id, missing);