@@ -23,28 +23,38 @@ def initialize(matches, players)
2323
2424 # Calculates complete performance data
2525 #
26+ # When player_id is provided, skips all team-level aggregations (overview,
27+ # trends, role breakdown, best performers) since the frontend only uses
28+ # player_stats in that context. This avoids 15+ unnecessary DB queries
29+ # per player-specific request.
30+ #
2631 # @param player_id [Integer, nil] Optional player ID for individual stats
2732 # @param all_players [ActiveRecord::Relation, nil] Scope to resolve the individual player
2833 # from. Defaults to @players (active only). Pass the full org scope when you want to
2934 # allow individual stats for inactive/bench/trial players too.
3035 # @return [Hash] Performance analytics data
3136 def calculate_performance_data ( player_id : nil , all_players : nil )
32- data = {
33- overview : team_overview ,
34- win_rate_trend : win_rate_trend ,
35- performance_by_role : performance_by_role ,
36- best_performers : best_performers ,
37- match_type_breakdown : match_type_breakdown
38- }
39-
4037 if player_id
4138 # Use the broader scope when provided so bench/trial players can still be looked up
4239 lookup_scope = all_players || @players
4340 player = lookup_scope . find_by ( id : player_id )
44- data [ :player_stats ] = player_statistics ( player ) if player
41+ return {
42+ overview : { } ,
43+ win_rate_trend : [ ] ,
44+ performance_by_role : [ ] ,
45+ best_performers : [ ] ,
46+ match_type_breakdown : [ ] ,
47+ player_stats : player ? player_statistics ( player ) : nil
48+ }
4549 end
4650
47- data
51+ {
52+ overview : team_overview ,
53+ win_rate_trend : win_rate_trend ,
54+ performance_by_role : performance_by_role ,
55+ best_performers : best_performers ,
56+ match_type_breakdown : match_type_breakdown
57+ }
4858 rescue StandardError => e
4959 Rails . logger . error ( "Error in calculate_performance_data: #{ e . message } " )
5060 Rails . logger . error ( e . backtrace . join ( "\n " ) )
@@ -59,29 +69,66 @@ def calculate_performance_data(player_id: nil, all_players: nil)
5969
6070 private
6171
62- # Calculates team overview statistics
72+ # Calculates team overview statistics using 2 aggregated SQL queries
73+ # instead of 10+ individual ones.
6374 def team_overview
64- stats = PlayerMatchStat . where ( match : @matches )
65- build_team_overview_hash ( stats )
75+ build_team_overview_hash
6676 rescue StandardError => e
6777 log_error ( "team_overview" , e )
6878 { }
6979 end
7080
71- def build_team_overview_hash ( stats )
81+ def build_team_overview_hash
82+ # Query 1: all match-level aggregates in a single pass
83+ match_row = @matches
84+ . select (
85+ 'COUNT(*) AS total' ,
86+ 'COUNT(*) FILTER (WHERE victory) AS wins' ,
87+ 'COUNT(*) FILTER (WHERE NOT victory) AS losses' ,
88+ 'ROUND(AVG(game_duration)) AS avg_duration'
89+ )
90+ . take
91+
92+ total = match_row &.total . to_i
93+ wins = match_row &.wins . to_i
94+ losses = match_row &.losses . to_i
95+ win_rate = total . zero? ? 0.0 : ( ( wins . to_f / total ) * 100 ) . round ( 1 )
96+
97+ # Query 2: all stat-level aggregates in a single pass, including sums for KDA
98+ stat_row = PlayerMatchStat
99+ . where ( match : @matches )
100+ . select (
101+ 'AVG(kills) AS avg_kills' ,
102+ 'AVG(deaths) AS avg_deaths' ,
103+ 'AVG(assists) AS avg_assists' ,
104+ 'AVG(gold_earned) AS avg_gold' ,
105+ 'AVG(damage_dealt_total) AS avg_damage' ,
106+ 'AVG(vision_score) AS avg_vision' ,
107+ 'SUM(kills) AS total_kills' ,
108+ 'SUM(deaths) AS total_deaths' ,
109+ 'SUM(assists) AS total_assists'
110+ )
111+ . take
112+
113+ total_kills = stat_row &.total_kills . to_i
114+ total_deaths = stat_row &.total_deaths . to_i
115+ total_assists = stat_row &.total_assists . to_i
116+ deaths_divisor = total_deaths . zero? ? 1 : total_deaths
117+ avg_kda = ( ( total_kills + total_assists ) . to_f / deaths_divisor ) . round ( 2 )
118+
72119 {
73- total_matches : @matches . count || 0 ,
74- wins : @matches . victories . count || 0 ,
75- losses : @matches . defeats . count || 0 ,
76- win_rate : calculate_win_rate ( @matches ) ,
77- avg_game_duration : @matches . average ( :game_duration ) &. round ( 0 ) || 0 ,
78- avg_kda : calculate_avg_kda ( stats ) ,
79- avg_kills_per_game : stats . average ( :kills ) &. round ( 1 ) || 0 ,
80- avg_deaths_per_game : stats . average ( :deaths ) &. round ( 1 ) || 0 ,
81- avg_assists_per_game : stats . average ( :assists ) &. round ( 1 ) || 0 ,
82- avg_gold_per_game : stats . average ( :gold_earned ) &. round ( 0 ) || 0 ,
83- avg_damage_per_game : stats . average ( :damage_dealt_total ) &. round ( 0 ) || 0 ,
84- avg_vision_score : stats . average ( :vision_score ) &. round ( 1 ) || 0
120+ total_matches : total ,
121+ wins : wins ,
122+ losses : losses ,
123+ win_rate : win_rate ,
124+ avg_game_duration : match_row &. avg_duration . to_i ,
125+ avg_kda : avg_kda ,
126+ avg_kills_per_game : stat_row &. avg_kills . to_f . round ( 1 ) ,
127+ avg_deaths_per_game : stat_row &. avg_deaths . to_f . round ( 1 ) ,
128+ avg_assists_per_game : stat_row &. avg_assists . to_f . round ( 1 ) ,
129+ avg_gold_per_game : stat_row &. avg_gold . to_f . round ( 0 ) ,
130+ avg_damage_per_game : stat_row &. avg_damage . to_f . round ( 0 ) ,
131+ avg_vision_score : stat_row &. avg_vision . to_f . round ( 1 )
85132 }
86133 end
87134
@@ -147,15 +194,14 @@ def performance_by_role
147194 end
148195
149196 # Identifies top performing players
150- # Single GROUP BY query instead of 1+6N per-player queries
197+ # Single GROUP BY query instead of 1+6N per-player queries.
198+ # Uses subqueries instead of pluck to avoid loading hundreds of IDs into Ruby.
151199 def best_performers
152- player_ids = @players . pluck ( :id )
153- match_ids = @matches . pluck ( :id )
154- return [ ] if player_ids . empty? || match_ids . empty?
200+ return [ ] if @players . none? || @matches . none?
155201
156202 aggregated = PlayerMatchStat
157203 . joins ( :match )
158- . where ( player_id : player_ids , match_id : match_ids )
204+ . where ( player_id : @players . select ( :id ) , match_id : @matches . select ( :id ) )
159205 . group ( :player_id )
160206 . select (
161207 'player_id' ,
@@ -298,22 +344,17 @@ def calculate_farm_share(stats)
298344 end
299345
300346 def sum_player_cs ( stats )
301- stats . sum (
302- "COALESCE(cs, minions_killed + COALESCE(jungle_minions_killed, 0), 0)"
303- ) . to_i
347+ stats . sum ( "COALESCE(cs, 0)" ) . to_i
304348 end
305349
306350 def calculate_team_cs ( stats )
307- match_ids = stats . pluck ( :match_id ) . uniq
308- return 0 if match_ids . empty?
309-
310351 player = stats . first &.player
311352 return 0 unless player &.organization_id
312353
313354 PlayerMatchStat
314355 . joins ( :player )
315- . where ( match_id : match_ids , players : { organization_id : player . organization_id } )
316- . sum ( "COALESCE(cs, minions_killed + COALESCE(jungle_minions_killed, 0), 0)" )
356+ . where ( match_id : stats . select ( :match_id ) , players : { organization_id : player . organization_id } )
357+ . sum ( "COALESCE(cs, 0)" )
317358 . to_i
318359 end
319360
0 commit comments