Team Scores in Football Tournament
Mechanism
Each match contributes points to two rows, so you fan one Matches row out to both participants with a LEFT JOIN on team_id = host_team OR team_id = guest_team, then collapse the fan-out with SUM(CASE …) that reads the match from whichever side (host or guest) the team played, awarding 3 / 1 / 0. The LEFT side keeps teams that never played (their only joined row is all-NULL, so the CASE falls through to 0).
Schema and the canonical query
Teams(team_id PK, team_name) and Matches(match_id PK, host_team, guest_team, host_goals, guest_goals). Every finished match is one row holding both teams and both scores; nobody's total lives in one place, which is exactly why a plain join won't do.
SELECT t.team_id,
t.team_name,
SUM(CASE
WHEN t.team_id = m.host_team AND m.host_goals > m.guest_goals THEN 3
WHEN t.team_id = m.guest_team AND m.guest_goals > m.host_goals THEN 3
WHEN m.host_goals = m.guest_goals THEN 1
ELSE 0
END) AS num_points
FROM Teams t
LEFT JOIN Matches m
ON t.team_id = m.host_team
OR t.team_id = m.guest_team
GROUP BY t.team_id, t.team_name
ORDER BY num_points DESC, t.team_id ASC;Note the third WHEN: by the time control reaches it, the team is provably a participant (it survived the join) and neither win-branch fired, so host_goals = guest_goals alone correctly catches the draw for either side. For a team with no matches, m.host_goals is NULL; every comparison is NULL (not true), so the CASE returns 0 and SUM of a single 0 is 0 — the team still appears.
Worked example with real values
Five teams; Chicago (40) plays no match. Five matches:
| match_id | host_team | guest_team | host_goals | guest_goals | result |
|---|---|---|---|---|---|
| 1 | 10 | 20 | 3 | 0 | 10 wins |
| 2 | 30 | 10 | 2 | 2 | draw |
| 3 | 10 | 50 | 5 | 1 | 10 wins |
| 4 | 20 | 30 | 1 | 0 | 20 wins |
| 5 | 50 | 30 | 1 | 0 | 50 wins |
After the OR-join, every match appears once per participant — match 1 produces one row for team 10 and one for team 20, never two identical rows for the same team. Here is the exact join output (one line per team-match), with the CASE evaluated:
| team_id | team_name | match_id | host_goals | guest_goals | role | CASE pts |
|---|---|---|---|---|---|---|
| 10 | DesignGuru FC | 1 | 3 | 0 | host | 3 |
| 10 | DesignGuru FC | 2 | 2 | 2 | guest | 1 |
| 10 | DesignGuru FC | 3 | 5 | 1 | host | 3 |
| 20 | NewYork FC | 1 | 3 | 0 | guest | 0 |
| 20 | NewYork FC | 4 | 1 | 0 | host | 3 |
| 30 | Atlanta FC | 2 | 2 | 2 | host | 1 |
| 30 | Atlanta FC | 4 | 1 | 0 | guest | 0 |
| 30 | Atlanta FC | 5 | 1 | 0 | guest | 0 |
| 40 | Chicago FC | NULL | NULL | NULL | — | 0 |
| 50 | Toranto FC | 3 | 5 | 1 | guest | 0 |
| 50 | Toranto FC | 5 | 1 | 0 | host | 3 |
GROUP BY then sums each team's CASE pts:
| team_id | team_name | sum of pts | num_points |
|---|---|---|---|
| 10 | DesignGuru FC | 3+1+3 | 7 |
| 20 | NewYork FC | 0+3 | 3 |
| 30 | Atlanta FC | 1+0+0 | 1 |
| 40 | Chicago FC | 0 | 0 |
| 50 | Toranto FC | 0+3 | 3 |
After ORDER BY num_points DESC, team_id ASC the final result is 10 (7), 20 (3), 50 (3), 30 (1), 40 (0) — the 20-before-50 tie broken by the smaller team_id, and Chicago surviving at 0 because of the LEFT join.
Why the naive version is wrong
A tempting shortcut writes the team into two rows itself — one fixed to the host slot, one to the guest slot — by joining twice or by a Cartesian-style condition that doesn't guard which side it scores. If the CASE then doesn't pin the comparison to the team's actual role, a single match can be counted twice for the same team, inflating points (the symptom: a phantom duplicate join row like 20 | 20 30 | 1 0 appearing twice for team 20, which the OR-join never produces — each match matches a given team on exactly one of the two OR arms). Always tie the win-test to the role: team_id = host_team AND host_goals > guest_goals, never just host_goals > guest_goals.
The UNION ALL alternative (and why it's often faster)
The OR in a join predicate is the expensive part: most planners cannot turn a = x OR a = y into a single index seek, so it degrades toward a nested-loop or hash join that touches the whole table. Splitting the two roles into a UNION ALL gives the optimizer two clean equi-joins, each index-friendly:
SELECT t.team_id, t.team_name,
COALESCE(SUM(s.pts), 0) AS num_points
FROM Teams t
LEFT JOIN (
SELECT host_team AS team_id,
CASE WHEN host_goals > guest_goals THEN 3
WHEN host_goals = guest_goals THEN 1 ELSE 0 END AS pts
FROM Matches
UNION ALL
SELECT guest_team AS team_id,
CASE WHEN guest_goals > host_goals THEN 3
WHEN guest_goals = host_goals THEN 1 ELSE 0 END AS pts
FROM Matches
) s ON s.team_id = t.team_id
GROUP BY t.team_id, t.team_name
ORDER BY num_points DESC, t.team_id ASC;It must be UNION ALL, not UNION: a 3-0 win and another 3-0 win are identical (team_id, pts) rows and UNION would dedup them, silently dropping points. The COALESCE replaces the all-NULL sum for teams with no matches.
Pitfalls
- Dropping no-match teams. An
INNER JOIN(or filtering onmatch_id IS NOT NULL) makes Chicago (40) vanish; the spec wants every team, including 0-point ones. UseLEFT JOINfromTeams. - Scoring the wrong side. Comparing goals without binding to the team's role (host vs guest) miscredits points and can double-count. Each
WHENfor a win must include theteam_id = …_teamguard. - Draw branch ordering. The bare
WHEN host_goals = guest_goals THEN 1only works because it sits after the two win branches and a non-participant row is alreadyNULLthere. Put it first and you'd award draws before checking wins — for a draw that's fine, but the dependency on order is a real footgun if you later edit the branches. - UNION vs UNION ALL. Using
UNIONin the split version deduplicates identical score rows and undercounts. AlwaysUNION ALLwhen summing. - OR-join cost at scale. On large
Matches, theORpredicate blocks index seeks; check the plan and prefer theUNION ALLform if you see a full scan or expensive join.
Takeaways
- When one fact row credits two parties, fan it out (OR-join or UNION ALL) then collapse with
SUM(CASE)— one row in, two scored rows, summed per party. - Bind every conditional credit to the row's actual role; a comparison without the role guard double-counts or miscredits.
LEFT JOINfrom the dimension you must fully enumerate (Teams) keeps zero-activity members;NULLcomparisons fall throughCASEto 0.ORin a join is a planner trap — theUNION ALLrewrite is equivalent, index-friendly, and usually faster, but must beALLto avoid dropping duplicate scores.
Problem: LeetCode 1212 “Team Scores in Football Tournament”. Mechanics of conditional aggregation, the OR-join planner cost, and the UNION ALL rewrite cross-checked against the PostgreSQL documentation (join methods, UNION/UNION ALL set semantics) and standard SQL CASE/NULL three-valued-logic behavior. Re-authored and deepened for this guide: the original Step-1 intermediate table contained a fabricated phantom duplicate row for team 20 that the OR-join cannot produce; replaced with a fully self-consistent five-team / five-match dataset, an end-to-end traced join-and-aggregate, a fan-out diagram, the ‘why the naive double-count is wrong’ note, and the UNION ALL alternative with its cost rationale.
🤖 Don't fully get this? Learn it with Claude
Stuck on Team Scores in Football Tournament? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.
Build the mental picture, not memorization.
I just read a lesson on **Team Scores in Football Tournament** (Databases) and want to truly understand it. Explain Team Scores in Football Tournament from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Socratic — adapts to where you're stuck.
Teach me **Team Scores in Football Tournament** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Active recall exposes what you missed.
Quiz me on **Team Scores in Football Tournament** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Intuition + hook + flashcards for long-term memory.
Help me remember **Team Scores in Football Tournament** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.