Back to feed

Project docs

Ski Jumping Statistics

PostgreSQL analysis of men's Ski Jumping World Cup results, supported by a Python data collection pipeline.

Competition Results

The final classification of a competition, including each athlete’s combined points, finishing position, and awarded World Cup points.

Rules

  • Points scored across all rounds are summed to produce each athlete’s final result.
  • Athletes are ranked by their combined points.
  • World Cup points are awarded to the top 30 finishers and later contribute to the overall season standings.
  • Athletes outside the top 30 receive zero World Cup points.

World Cup scoring system

  • 1st → 100
  • 2nd → 80
  • 3rd → 60
  • 4th → 50
  • 5th → 45
  • 6th → 40
  • 7th → 36
  • 8th → 32
  • 9th → 29
  • 10th → 26
  • 11th–30th → decreasing from 24 to 1

Reusable competition results

Competition results form the foundation of many later statistics. To make them faster and easier to reuse, the aggregated data is stored in a materialized view.

create materialized view competition_results
as (
	with competition_total_points as (
		select r.race_id, r.jumper_id, SUM(r.total_points) as total_points
		from results r
		group by r.race_id, r.jumper_id
	),
	results_ranked as (
		select *,
			rank() over (partition by race_id order by total_points desc) as pos
		from competition_total_points
	),
	competition_scoring as (
	    SELECT * FROM (VALUES
	        (1, 100), (2, 80), (3, 60), (4, 50), (5, 45),
	        (6, 40),  (7, 36), (8, 32), (9, 29), (10, 26),
	        (11, 24), (12, 22), (13, 20), (14, 18), (15, 16),
	        (16, 15), (17, 14), (18, 13), (19, 12), (20, 11),
	        (21, 10), (22, 9),  (23, 8),  (24, 7),  (25, 6),
	        (26, 5),  (27, 4),  (28, 3),  (29, 2),  (30, 1)
	    ) AS t(pos, score)
	)
	select r.*, coalesce(cs.score, 0) as score
	from results_ranked r
	left join competition_scoring cs on r.pos = cs.pos
	order by race_id, total_points desc
)
with data;

View presentation

Raw competition_results view returned by PostgreSQL0 rows

Refreshing the view

PostgreSQL does not refresh materialized views automatically when their source data changes. A statement-level trigger therefore refreshes competition_results after new rows are inserted into the results table.

create function refresh_competition_results()
	returns trigger
	language plpgsql
as $$
begin
	refresh materialized view competition_results;
	return null;
end $$;

create trigger results_insert
	after insert
	on results
	for each statement
	execute function refresh_competition_results();

Refreshing a materialized view through a trigger would often be too expensive for a frequently updated table. Here, however, results change only when the web scraping pipeline loads new competitions, making the trade-off acceptable for this project.

Presenting a competition

The materialized view can now be combined with athlete data and find_race_id to present the competition held in Bischofshofen, Austria, in 2025:

select
	cr.pos,
	j.jumper_name || ' (' || j.jumper_country || ')' as jumper,
	cr.total_points::decimal(10, 2),
	cr.score
from competition_results cr
join jumpers j using (jumper_id)
where race_id = find_race_id(2025, 'Bischofshofen', 1);

Results

Competition results returned by PostgreSQL0 rows