From 7de106b2dccf656d7e48735e33a31a8b099df040 Mon Sep 17 00:00:00 2001 From: senke Date: Sat, 14 Feb 2026 18:31:29 +0100 Subject: [PATCH] perf(analytics): optimize GetTrackStats to single query --- .../internal/services/analytics_service.go | 78 ++++++++----------- 1 file changed, 33 insertions(+), 45 deletions(-) diff --git a/veza-backend-api/internal/services/analytics_service.go b/veza-backend-api/internal/services/analytics_service.go index 0b2bdf3d7..8bd928fa5 100644 --- a/veza-backend-api/internal/services/analytics_service.go +++ b/veza-backend-api/internal/services/analytics_service.go @@ -91,57 +91,45 @@ func (s *AnalyticsService) RecordPlay(ctx context.Context, trackID uuid.UUID, us return nil } -// GetTrackStats récupère les statistiques d'un track -func (s *AnalyticsService) GetTrackStats(ctx context.Context, trackID uuid.UUID) (*types.TrackStats, error) { // Changed trackID to uuid.UUID - var stats types.TrackStats - - // Vérifier que le track existe - var track models.Track - if err := s.db.WithContext(ctx).First(&track, "id = ?", trackID).Error; err != nil { // Updated query - if err == gorm.ErrRecordNotFound { - return nil, errors.New("track not found") - } - return nil, fmt.Errorf("failed to get track: %w", err) +// GetTrackStats récupère les statistiques d'un track (optimisé: 1 requête au lieu de 5) +func (s *AnalyticsService) GetTrackStats(ctx context.Context, trackID uuid.UUID) (*types.TrackStats, error) { + var result struct { + TrackDuration int `gorm:"column:track_duration"` + TotalPlays int64 `gorm:"column:total_plays"` + UniqueListeners int64 `gorm:"column:unique_listeners"` + AvgDuration float64 `gorm:"column:avg_duration"` + CompletedPlays int64 `gorm:"column:completed_plays"` } - // Total plays - if err := s.db.WithContext(ctx).Model(&models.TrackPlay{}). - Where("track_id = ?", trackID). - Count(&stats.TotalPlays).Error; err != nil { - return nil, fmt.Errorf("failed to count total plays: %w", err) + tx := s.db.WithContext(ctx).Raw(` + SELECT + t.duration AS track_duration, + CAST(COUNT(tp.id) AS INTEGER) AS total_plays, + CAST(COUNT(DISTINCT CASE WHEN tp.user_id IS NOT NULL THEN tp.user_id END) AS INTEGER) AS unique_listeners, + COALESCE(AVG(tp.duration), 0) AS avg_duration, + CAST(COUNT(CASE WHEN t.duration > 0 AND tp.duration >= t.duration * 0.9 THEN 1 END) AS INTEGER) AS completed_plays + FROM tracks t + LEFT JOIN track_plays tp ON tp.track_id = t.id + WHERE t.id = ? + GROUP BY t.id, t.duration + `, trackID).Scan(&result) + if tx.Error != nil { + return nil, fmt.Errorf("failed to get track stats: %w", tx.Error) + } + if tx.RowsAffected == 0 { + return nil, errors.New("track not found") } - // Unique listeners (distinct user_id, en excluant NULL) - if err := s.db.WithContext(ctx).Model(&models.TrackPlay{}). - Where("track_id = ? AND user_id IS NOT NULL", trackID). - Distinct("user_id"). - Count(&stats.UniqueListeners).Error; err != nil { - return nil, fmt.Errorf("failed to count unique listeners: %w", err) + stats := &types.TrackStats{ + TotalPlays: result.TotalPlays, + UniqueListeners: result.UniqueListeners, + AverageDuration: result.AvgDuration, + CompletionRate: 0, } - - // Average duration - var avgDuration float64 - if err := s.db.WithContext(ctx).Model(&models.TrackPlay{}). - Where("track_id = ?", trackID). - Select("COALESCE(AVG(duration), 0)"). - Scan(&avgDuration).Error; err != nil { - return nil, fmt.Errorf("failed to calculate average duration: %w", err) + if result.TotalPlays > 0 { + stats.CompletionRate = float64(result.CompletedPlays) / float64(result.TotalPlays) * 100 } - stats.AverageDuration = avgDuration - - // Completion rate (90% de la durée du track) - if track.Duration > 0 && stats.TotalPlays > 0 { - var completedPlays int64 - completionThreshold := int(float64(track.Duration) * 0.9) - if err := s.db.WithContext(ctx).Model(&models.TrackPlay{}). - Where("track_id = ? AND duration >= ?", trackID, completionThreshold). - Count(&completedPlays).Error; err != nil { - return nil, fmt.Errorf("failed to count completed plays: %w", err) - } - stats.CompletionRate = float64(completedPlays) / float64(stats.TotalPlays) * 100 - } - - return &stats, nil + return stats, nil } // GetPlaysOverTime récupère les lectures sur une période pour un graphique temporel