Home > Software design >  Finding or calculating the stats on a tournament
Finding or calculating the stats on a tournament

Time:08-07

This problem was bugging me for the last couple of weeks, but the solution I've reached so far doesn't seem good enough to aleviate the doubts I have.

Problem Statement: Build a table (query) that shows the followings columns (team name, number of matches, victories, defeats, draws, and score) the score of each team is calculated knowing that each victory gives 3 points and a draw gives one point, while defeats don't afect at all.

Table Schemas:

Table "teams":

Column name Type
id int
name varchar(50)

Table "matches":

Column name Type
id int
team_1 int
team_2 int
team_1_goals int
team_2_goals int

Sample Data:

Table "teams":

id name
1 CEARA
2 FORTALEZA
3 GUARANY DE SOBRAL
4 FLORESTA

Table "matches":

id team_1 team_2 team_1_goals team_2_goals
1 4 1 0 4
2 3 2 0 1
3 1 3 3 0
4 3 4 0 1
5 1 2 0 0
6 2 4 2 1

Expected Output:

name matches victories defeats draws score
CEARA 3 2 0 1 7
FORTALEZA 3 2 0 1 7
FLORESTA 3 1 2 0 3
GUARANY DE SOBRAL 3 0 3 0 0

What I've managed to do so far:

SELECT
    t.name, 
    count(m.team_1) filter(WHERE t.id = m.team_1)
          count(m.team_2) filter(WHERE t.id = m.team_2) "matches",
    count(m.team_1) filter(WHERE t.id = m.team_1 AND m.team_1_goals > m.team_2_goals)
          count(m.team_2) filter(WHERE t.id = m.team_2 AND m.team_1_goals < m.team_2_goals) "victories",
    count(m.team_1) filter(WHERE t.id = m.team_1 AND m.team_1_goals < m.team_2_goals)
          count(m.team_2) filter(WHERE t.id = m.team_2 AND m.team_1_goals > m.team_2_goals) "defeats",
    count(m.team_1) filter(WHERE t.id = m.team_1 AND m.team_1_goals = m.team_2_goals)
          count(m.team_2) filter(WHERE t.id = m.team_2 AND m.team_1_goals = m.team_2_goals) "draws",
    ((count(m.team_1) filter(WHERE t.id = m.team_1 AND m.team_1_goals > m.team_2_goals)
          count(m.team_2) filter(WHERE t.id = m.team_2 AND m.team_1_goals < m.team_2_goals))* 3)  
        count(m.team_1) filter(WHERE t.id = m.team_1 AND m.team_1_goals = m.team_2_goals)
          count(m.team_2) filter(WHERE t.id = m.team_2 AND m.team_1_goals = m.team_2_goals) "score"
FROM
    teams t
JOIN matches m ON t.id IN (m.team_1, m.team_2)
GROUP BY t.name
ORDER BY "victories" DESC

which theoretically outputs the correct answer.

I've tried to make it by using fancy things like CASE WHEN or a bigger JOIN but with no good results. What I want to know is if there is a better way to do this query in terms of writing and performance in the server.

Appreciate any help!

CodePudding user response:

One option is to

  • unify the "matches" fields into "team" and "goals" twice (one for each team), while keeping trace of the wins (1 if team A won on team B, 0 for draws, -1 for lost match)
  • apply a LEFT JOIN between "teams" table and the just-updated "matches" table
  • use all needed aggregations in combination of CASE statements, as in the example below (COALESCE will convert any null value into zero).
WITH cte_matches AS (
    SELECT id, 
           team_1                                          AS team, 
           team_1_goals                                    AS goals, 
           CASE WHEN team_1_goals > team_2_goals THEN 1
                WHEN team_1_goals < team_2_goals THEN -1
                ELSE 0 END                                 AS has_won
    FROM matches
    UNION ALL
    SELECT id, 
           team_2                                          AS team, 
           team_2_goals                                    AS goals, 
           CASE WHEN team_2_goals > team_1_goals THEN 1
                WHEN team_2_goals < team_1_goals THEN -1
                ELSE 0 END                                 AS has_won
    FROM matches
)
SELECT t.name, 
       COUNT(t.id)      AS matches,
       COALESCE(SUM(CASE WHEN has_won =  1 THEN 1 END), 0) AS victories,
       COALESCE(SUM(CASE WHEN has_won = -1 THEN 1 END), 0) AS defeats,
       COALESCE(SUM(CASE WHEN has_won =  0 THEN 1 END), 0) AS draws,
       COALESCE(SUM(CASE WHEN has_won =  1 THEN 3
                         WHEN has_won =  0 THEN 1 END), 0) AS score
FROM      teams       t
LEFT JOIN cte_matches m
       ON t.id = m.team
GROUP BY t.name

Check the demo here.

CodePudding user response:

I normalized the layout of the matches table to simplify things, but I assume that the structure of that table was given to you. Please let me know if you have control over the structure of the matches table, and I can show you how to use lead() to determine the win/loss/draw status for each side of a match.

If you represent the point value as a table (or a CTE, common table expression, in this case), you can simplify the aggregations:

with norm_matches as (
  select id as match_id, team_1 as team_id, team_1_goals as goals, 
         case
           when team_1_goals > team_2_goals then 'W'
           when team_1_goals = team_2_goals then 'D'
           when team_1_goals < team_2_goals then 'L'
         end as outcome
    from matches
  union all
  select id as match_id, team_2 as team_id, team_2_goals as goals, 
         case
           when team_1_goals > team_2_goals then 'L'
           when team_1_goals = team_2_goals then 'D'
           when team_1_goals < team_2_goals then 'W'
         end as outcome
    from matches
), points (outcome, value) as (
  values ('W', 3), ('D', 1), ('L', 0)
)
select t.name, 
       count(1) as matches,
       count(1) filter (where m.outcome = 'W') as victories,
       count(1) filter (where m.outcome = 'L') as defeats,
       count(1) filter (where m.outcome = 'D') as draws,
       sum(p.value) as score
  from teams t
       join norm_matches m on m.team_id = t.id
       join points p on p.outcome = m.outcome
 group by t.name
 order by t.name
;

db<>fiddle here

  • Related