perf(backend): индексы, кэш чартов, пропуск upsert, фикс N+1 обогащения

- track_plays(played_at,track_id,station_id) покрывающий + tracks(first_seen_at) WHERE
  pending частичный (применены CONCURRENTLY на проде + миграция idempotent)
- ChartsService.getTopTracks: in-memory TTL-кэш 90с по (period,genre,limit) → детальная
  страница и параллельные запросы не пересчитывают тяжёлые агрегации
- NowPlayingService.ingest: не пишем now_playing и не шлём сокет, если трек не изменился
  (было ~20k бесполезных upsert/час)
- enrichNowPlaying: вместо N+1 (300 upsert/мин) — один batched findMany по normKey

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
nk
2026-06-06 16:20:25 +03:00
parent 924a4a0ab1
commit a3434ed894
5 changed files with 72 additions and 13 deletions

View File

@@ -0,0 +1,5 @@
-- Покрывающий индекс под агрегацию чартов (идемпотентно — на проде уже создан CONCURRENTLY).
CREATE INDEX IF NOT EXISTS "track_plays_window_idx" ON "track_plays" ("played_at", "track_id", "station_id");
-- Частичный индекс под бэкфилл обогащения (Prisma не выражает partial WHERE в схеме).
CREATE INDEX IF NOT EXISTS "tracks_enrich_pending_idx" ON "tracks" ("first_seen_at" DESC) WHERE "enrich_status" = 'pending';

View File

@@ -139,6 +139,8 @@ model Track {
likes TrackLike[]
@@index([genre])
// Частичный индекс под бэкфилл обогащения создаётся миграцией (Prisma не умеет
// partial WHERE): tracks_enrich_pending_idx (first_seen_at DESC) WHERE enrich_status='pending'.
@@map("tracks")
}
@@ -154,6 +156,8 @@ model TrackPlay {
@@index([trackId, playedAt])
@@index([playedAt])
@@index([stationId])
// Покрывающий индекс под агрегацию чартов (WHERE played_at>=X → GROUP BY track_id, COUNT DISTINCT station_id)
@@index([playedAt, trackId, stationId], map: "track_plays_window_idx")
@@map("track_plays")
}