perf: replace Prisma relation queries with raw SQL for getExistingMatchIds and getMissingScopes - fixes Pi hang
Deploy Iddaai Backend / build-and-deploy (push) Successful in 39s

This commit is contained in:
2026-04-26 17:07:19 +03:00
parent bc461429f6
commit 691c52f610
@@ -853,49 +853,34 @@ export class FeederPersistenceService {
} }
async getExistingMatchIds(matchIds: string[]): Promise<string[]> { async getExistingMatchIds(matchIds: string[]): Promise<string[]> {
const matches = await this.prisma.match.findMany({ if (matchIds.length === 0) return [];
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 },
});
const footballIds = matches // Use raw SQL for performance — Prisma's { some: {} } relation filters
.filter((m) => m.sport === "football") // generate heavy correlated subqueries that hang on Raspberry Pi with
.map((m) => m.id); // large tables (15M+ odd_selections, 3M+ participations).
const completeFootballIds = new Set<string>(); 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) { return result.map((r) => r.id);
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);
} }
/** /**
@@ -909,38 +894,45 @@ export class FeederPersistenceService {
const result = new Map<string, string[]>(); const result = new Map<string, string[]>();
if (matchIds.length === 0) return result; if (matchIds.length === 0) return result;
const matches = await this.prisma.match.findMany({ // Use raw SQL for performance on Raspberry Pi.
where: { // Note: state is 'postGame' in DB, not 'Ended'.
id: { in: matchIds }, const rows = await this.prisma.$queryRawUnsafe<
state: "Ended", Array<{
}, id: string;
select: { sport: string;
id: true, fts_count: bigint;
sport: true, pp_count: bigint;
_count: { bts_count: bigint;
select: { bps_count: bigint;
playerParticipations: true, oc_count: bigint;
footballTeamStats: true, }>
basketballTeamStats: true, >(
basketballPlayerStats: true, `
oddCategories: true, 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[] = []; const missing: string[] = [];
if (m.sport === "football") { if (m.sport === "football") {
if (m._count.footballTeamStats === 0) missing.push("stats"); if (Number(m.fts_count) === 0) missing.push("stats");
if (m._count.playerParticipations < 18) missing.push("lineups"); if (Number(m.pp_count) < 18) missing.push("lineups");
} else if (m.sport === "basketball") { } else if (m.sport === "basketball") {
if (m._count.basketballTeamStats === 0) missing.push("stats"); if (Number(m.bts_count) === 0) missing.push("stats");
if (m._count.basketballPlayerStats === 0) missing.push("lineups"); 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) { if (missing.length > 0) {
result.set(m.id, missing); result.set(m.id, missing);