f1 predict
live
./docs / database-schema

Database Schema

Provider: Neon PostgreSQL
ORM: Drizzle
Schema source: api/src/db/schema/
Migrations: db/ (generated SQL files)


Conventions

  • All primary keys are SERIAL (auto-increment integer)
  • All timestamps are TIMESTAMPTZ DEFAULT now()
  • Lap times stored as INTEGER milliseconds — never float
  • Feature scores stored as NUMERIC(6,5) — range 0.00000 to 1.00000
  • All natural keys have UNIQUE constraints

Table Overview

seasons
  └── teams          (season-scoped)
  └── drivers        (season-scoped, FK → teams)
  └── races          (FK → circuits)
        └── qualifying_results      (FK → drivers)
        └── race_results            (FK → drivers)
        └── lap_times               (FK → drivers)
        └── driver_prediction_features (FK → drivers)
        └── race_predictions        (FK → drivers)
        └── sprint_results          (FK → drivers)  ← sprint weekends only
        └── sprint_lap_times        (FK → drivers)  ← sprint weekends only
        └── driver_sprint_features  (FK → drivers)  ← sprint weekends only
        └── sprint_predictions      (FK → drivers)  ← sprint weekends only
  └── driver_season_stats  (FK → drivers)
  └── team_season_stats    (FK → teams)

circuits   (static, seeded once)

Tables

seasons

One row per calendar year.

ColumnTypeNotes
idserial PK
yearinteger UNIQUEe.g. 2026

circuits

Static track data, seeded once. Not season-scoped.

ColumnTypeNotes
idserial PK
circuit_keyvarchar(50) UNIQUEe.g. monza, silverstone
namevarchar(100)
countryvarchar(50)
cityvarchar(50)
lap_countinteger
track_length_kmnumeric(5,3)
overtake_ratenumeric(4,3)0.0–1.0; used in prediction model
number_of_cornersintegernull for pre-DRS-era circuits
drs_zonesintegernull for pre-DRS-era circuits
sc_probabilitynumeric(4,3)Historical SC deployment rate (completed races); null for circuits with no completed races

teams

Season-scoped — a team row exists per year it competed.

ColumnTypeNotes
idserial PK
season_idFK → seasons
team_keyvarchar(50)snake_case, e.g. red_bull
namevarchar(100)Display name
nationalityvarchar(50)
UNIQUE(season_id, team_key)

drivers

Season-scoped — a driver row exists per year they raced.

ColumnTypeNotes
idserial PK
season_idFK → seasons
team_idFK → teams
driver_numberinteger
codechar(3)e.g. VER, HAM
first_namevarchar(50)
last_namevarchar(50)
nationalityvarchar(50)
headshot_urlvarchar(255)Populated from FastF1 for 2018+
UNIQUE(season_id, driver_number)

races

One row per race event. Sprint weekends have one race row (covering both the sprint and grand prix).

ColumnTypeNotes
idserial PK
season_idFK → seasons
circuit_idFK → circuits
round_numberinteger
namevarchar(100)e.g. Monaco Grand Prix
race_datedateGrand Prix date
qualifying_datedateSaturday qualifying date
sprint_datedateSprint race date (sprint weekends only)
sprint_qualifying_datedateSQ session date (sprint weekends only)
event_formatvarchar(30)conventional | sprint | sprint_qualifying | sprint_shootout
statusvarchar(30)See status flow below
weathervarchar(30)dry | wet | mixed — main race weather
safety_car_lapsinteger2018+ only
vsc_lapsinteger2018+ only
air_temp_avgnumeric(4,1)2018+ only
track_temp_avgnumeric(4,1)2018+ only
humidity_avgnumeric(4,1)2018+ only
sprint_weathervarchar(30)Sprint-specific weather — sprint weekends only
sprint_safety_car_lapsintegerSprint SC laps
sprint_vsc_lapsintegerSprint VSC laps
sprint_air_temp_avgnumeric(4,1)Sprint session air temp
sprint_track_temp_avgnumeric(4,1)Sprint session track temp
sprint_humidity_avgnumeric(4,1)Sprint session humidity
UNIQUE(season_id, round_number)

Race Status Flow

conventional weekend:
  scheduled → qualifying_done → completed

sprint weekend:
  scheduled → sprint_qualifying_done → sprint_done → qualifying_done → completed
                      ↓                     ↓                ↓
               sprint features      ingest sprint       main qualifying
               sprint predictions   sprint season stats features + predictions

qualifying_results

One row per driver per main qualifying session.

ColumnTypeNotes
idserial PK
race_idFK → races
driver_idFK → drivers
grid_positionintegerFinal starting grid position
q1_time_msintegerNull if knocked out before Q2
q2_time_msintegerNull if knocked out before Q3
q3_time_msintegerNull if not in Q3
sector1_msintegerBest sector times — 2018+ only
sector2_msinteger
sector3_msinteger
speed_stnumeric(5,1)Speed trap km/h — 2018+ only
UNIQUE(race_id, driver_id)

race_results

One row per driver per grand prix.

ColumnTypeNotes
idserial PK
race_idFK → races
driver_idFK → drivers
finish_positionintegerNull = DNF/DSQ
grid_positioninteger
pointsnumeric(4,1)Championship points scored
statusvarchar(30)e.g. Finished, +1 Lap, Accident
total_race_time_msbigintFor winner only
fastest_lapboolean
UNIQUE(race_id, driver_id)

lap_times

One row per lap per driver — 2018+ only.

ColumnTypeNotes
idbigserial PKLarge table — bigserial not serial
race_idFK → races
driver_idFK → drivers
lap_numberinteger
lap_time_msinteger
sector1_msinteger
sector2_msinteger
sector3_msinteger
speed_stnumeric(5,1)Speed trap km/h
compoundvarchar(20)SOFT, MEDIUM, HARD, INTER, WET
tyre_lifeintegerLaps on current tyre
fresh_tyreboolean
is_pit_lapbooleanExcluded from pace calculations
stint_numberintegerStint index within the race — used for tyre degradation slope
UNIQUE(race_id, driver_id, lap_number)

sprint_results

One row per driver per sprint race. Also stores sprint qualifying (SQ) session times.

ColumnTypeNotes
idserial PK
race_idFK → races
driver_idFK → drivers
finish_positionintegerNull = DNF/DSQ
grid_positionintegerSet from SQ result
pointsnumeric(4,1)Sprint points scored
statusvarchar(30)e.g. Finished, +1 Lap
total_sprint_time_msbigintFor sprint winner only
fastest_lapboolean
sq1_time_msintegerSQ1 lap time — null if eliminated in SQ1
sq2_time_msintegerSQ2 lap time — null if not in SQ2
sq3_time_msintegerSQ3 lap time — null if not in SQ3
sq_sector1_msintegerBest S1 from SQ session
sq_sector2_msintegerBest S2 from SQ session
sq_sector3_msintegerBest S3 from SQ session
sq_speed_stnumeric(5,1)Max speed trap from SQ session
UNIQUE(race_id, driver_id)

sprint_lap_times

Per-lap data for sprint races — mirrors lap_times structure. 2018+ only.

ColumnTypeNotes
idbigserial PK
race_idFK → races
driver_idFK → drivers
lap_numberinteger
lap_time_msinteger
sector1_msinteger
sector2_msinteger
sector3_msinteger
speed_stnumeric(5,1)
compoundvarchar(20)
tyre_lifeinteger
fresh_tyreboolean
is_pit_lapboolean
stint_numberintegerStint index within the sprint
UNIQUE(race_id, driver_id, lap_number)

fp2_long_run_times

FP2 long-run stint data per driver per race. Populated by ingest_fp2. 2018+ only.

ColumnTypeNotes
idserial PK
race_idFK → races
driver_idFK → drivers
compoundvarchar(20)SOFT, MEDIUM, HARD
median_lap_msintegerMEDIUM-normalised median stint lap time (Soft +500ms, Hard −400ms)
stint_lengthintegerNumber of laps in the long-run stint (≥5)
fp2_best_lap_msintegerDriver’s single fastest raw lap in FP2
UNIQUE(race_id, driver_id, compound)Best (shortest) stint per compound kept

driver_season_stats

Aggregated after each race via compute_season_stats. Includes sprint-specific aggregates.

ColumnTypeNotes
season_idFK → seasons
driver_idFK → drivers
races_enteredintegerGrand prix count
winsintegerGrand prix wins
podiumsinteger
polesintegerGrid position = 1
total_pointsnumericGrand prix points
championship_positionintegerRanked by total_points
win_ratenumeric(5,4)Bayesian smoothed
avg_position_gainnumeric(4,2)grid_position − finish_position avg
dnf_ratenumeric(4,3)dnf_count / races_entered
avg_sector1/2/3_msintegerMedian — 2018+ only
teammate_quali_deltanumeric(6,4)Mean delta vs teammate across season
sprint_races_enteredintegerSprint race count
sprint_winsinteger
sprint_podiumsinteger
sprint_total_pointsnumericSprint points
sprint_win_ratenumeric(5,4)Bayesian smoothed sprint win rate
UNIQUE(season_id, driver_id)

team_season_stats

Aggregated after each race. Drives car_performance_score in predictions.

ColumnTypeNotes
season_idFK → seasons
team_idFK → teams
car_performance_scorenumeric(5,4)Normalized from avg finish position
reliability_scorenumeric(5,4)Normalized from 1 − dnf_rate
championship_positioninteger
UNIQUE(season_id, team_id)

driver_prediction_features

One row per driver per grand prix race. Written by compute_features, updated by compute_predictions.

ColumnTypeNotes
race_idFK → races
driver_idFK → drivers
car_performance_scorenumeric(6,5)0–1
driver_rating_scorenumeric(6,5)
starting_position_scorenumeric(6,5)
win_rate_scorenumeric(6,5)
luck_factor_scorenumeric(6,5)
weather_impact_scorenumeric(6,5)
track_overtake_scorenumeric(6,5)Deprecated — always NULL in weighted-v3; value baked into circuit-adjusted scores
position_gain_scorenumeric(6,5)Raw avg position gain (kept for display only)
long_run_pace_scorenumeric(6,5)FP2 primary, historical fallback
reliability_scorenumeric(6,5)
qualifying_delta_scorenumeric(6,5)Rolling 5-race weighted teammate delta
sector_strength_scorenumeric(6,5)
tyre_deg_scorenumeric(6,5)REGR_SLOPE-derived degradation score — lower slope = higher score
circuit_adj_start_pos_scorenumeric(6,5)Starting position scaled by overtake_rate × sc_probability
circuit_adj_position_gain_scorenumeric(6,5)Position gain scaled by overtake_rate
raw_weighted_scorenumeric(8,6)Weighted sum before softmax
win_probabilitynumeric(6,5)After softmax — sums to 1.0 per race
predicted_positioninteger1 = predicted winner
UNIQUE(race_id, driver_id)

race_predictions

One row per grand prix — the single predicted winner.

ColumnTypeNotes
race_idFK → races UNIQUE
predicted_winner_idFK → drivers
computed_attimestamptz
model_versionvarchar(20)weighted-v3

driver_sprint_features

One row per driver per sprint weekend. Written by compute_sprint_features, updated by compute_sprint_predictions.

ColumnTypeNotes
race_idFK → races
driver_idFK → drivers
car_performance_scorenumeric(6,5)0–1
starting_position_scorenumeric(6,5)Raw SQ grid score (kept for display)
driver_rating_scorenumeric(6,5)Sprint-specific when ≥3 sprint races recorded
track_overtake_scorenumeric(6,5)Deprecated — always NULL in sprint-v2
short_run_pace_scorenumeric(6,5)Best SQ lap time, falls back to main qualifying
weather_impact_scorenumeric(6,5)Based on sprint_weather field; cross-season
win_rate_scorenumeric(6,5)Sprint-specific when ≥3 sprint races recorded
luck_factor_scorenumeric(6,5)Rolling 5-race delta — cross-season
circuit_adj_start_pos_scorenumeric(6,5)SQ grid scaled by overtake_rate × sc_probability
sq_qualifying_delta_scorenumeric(6,5)Rolling 5-sprint SQ teammate delta
raw_weighted_scorenumeric(8,6)
win_probabilitynumeric(6,5)After softmax
predicted_positioninteger
UNIQUE(race_id, driver_id)

sprint_predictions

One row per sprint weekend — the single predicted sprint winner.

ColumnTypeNotes
race_idFK → races UNIQUE
predicted_winner_idFK → drivers
computed_attimestamptz
model_versionvarchar(20)sprint-v2