Formula 1 analytics pipeline
Every Formula 1 season since 1950, pulled from a rate-limited public API, landed immutably in S3, loaded into Postgres, and modelled into a tested Kimball star schema with dbt. Orchestrated nightly by Airflow, served through Metabase.
01 — the pipeline
The API is rate limited to 500 requests an hour, so the landing zone is immutable: raw responses go to S3 once and are never re-fetched. Everything downstream can be rebuilt from that copy without spending the budget again.
source
public API
500 requests / hour
landing
S3
Immutable raw responses
warehouse
Postgres
Loaded, then staged
model
dbt star schema
Facts, dimensions, tests
serving
Metabase
Dashboards, ad hoc SQL
orchestration Airflow: nightly, one DAG per stage, retries on the rate limit
02 — the model
Two fact tables at different grains, sharing conformed dimensions. Sprint results are a separate fact rather than a session_type column, because a fact whose grain needs a filter to be correct is a reliable source of wrong numbers.
- FKrace_key → dim_race
- FKdriver_key → dim_driver
- FKconstructor_key → dim_constructor
- finish_position
- points
- status
- FKrace_key → dim_race
- FKdriver_key → dim_driver
- points
- PKdriver_key
- driver_name
- nationality
- PKconstructor_key
- constructor_name
- base_country
- PKrace_key
- season
- round
- FKcircuit_key → dim_circuit
- PKcircuit_key
- circuit_name
- country
Both facts point at the same dimensions, so a sprint weekend and a race weekend can be counted together. dim_circuit hangs off dim_race rather than off the facts: a race happens at one circuit, so the facts reach it through the race they already point at. Both calls are recorded in the repository with the alternatives that lost, so the diagram can be checked against the code rather than taken on trust.
03 — the test that earned its keep
One dbt test derives each championship total from the facts and compares it against the official standings. A row count would have passed. This did not.
WITH derived AS (
SELECT season, driver_key, SUM(points) AS derived_points
FROM fct_all_points -- race + sprint
GROUP BY 1, 2
)
SELECT s.season, s.official_points, d.derived_points
FROM stg_official_standings s
JOIN derived d USING (season, driver_key)
WHERE s.official_points <> d.derived_points;
| season | official | derived | delta |
|---|---|---|---|
| 2024 | 437 | 399 | −38 |
Sprint points were missing entirely. The fix was a second fact table and a union at the points layer. Without the test, nobody would have known to write it.