← Football data warehouse
7 min readPart 2 of 4

Setting up the project and building the bronze layer

Part 2: requirements, naming conventions, the database, and loading raw Transfermarkt data into PostgreSQL exactly as it arrives.

Data Warehouse SeriesData EngineeringSQLFootball Analytics

The requirements

Every data project starts with understanding what you need to build. This project had two goals.

1 · Build the warehouse (data engineering)

  • Build a modern data warehouse in PostgreSQL
  • Sources: Transfermarkt data from Kaggle, as CSV files about players, games and competitions
  • Quality: cleanse and fix data issues before analysis
  • Integration: combine the sources into one user-friendly model
  • Scope: the latest dataset only — no historisation needed
  • Documentation: clearly document the data model

2 · Analytics and reporting (data analysis)

Build SQL-based analytics with insights into:

  • Team behaviour
  • Player performance
  • Match predictions

Planning the work

I broke the project into epics, each with analysing, coding, validating and documenting steps.

EpicTasks
Requirements analysisAnalyse and understand the requirements
Design the architectureChoose an approach · design the layers · draw the architecture
Project initialisationDetailed tasks · naming conventions · Git repository · database and schemas
Bronze layerAnalyse source systems · ingest data · check completeness and schema · draw the data flow · commit
Silver layerExplore the data · clean it · check correctness · commit
Gold layerExplore business objects · integrate data · check integration · draw the star schema · create a data catalogue · extend the data flow · commit

Naming conventions

Without agreed rules, names quickly drift: one developer writes dimension_players, another DimensionClubs, another dim_Competitions. Everything becomes harder to query. So I set rules up front.

ObjectPatternExample
Bronze and silver tables<source>_<entity> — keep the original table nameplayers_transfers
Gold dimension tablesdim_<entity>dim_player
Gold fact tablesfact_<entity>fact_transfers
Aggregated tablesagg_<entity>agg_transfers_monthly
Surrogate keys<table>_keyplayer_key
Technical columnsdwh_<column>dwh_created_at
Load proceduresload_<layer>load_silver

Repository, database and schemas

I created a Git repository with a folder for each layer's scripts, then created the FootballDataWarehouse database in pgAdmin 4. One schema per layer keeps everything separate:

-- Create schemas for the medallion architecture
CREATE SCHEMA IF NOT EXISTS bronze;
CREATE SCHEMA IF NOT EXISTS silver;
CREATE SCHEMA IF NOT EXISTS gold;

Building the bronze layer

The bronze layer stores the raw data exactly as it arrives. There are four steps: analyse the source, write the ingestion code, validate it, and document it.

1. Analyse the source systems

In a real organisation, you'd interview the people who own the source systems. These are the questions worth asking:

Business context

  • Who owns the data?
  • What process does it support?
  • Is there documentation, a data model or a catalogue?

Technology

  • Where is it stored? (Postgres, Oracle, cloud…)
  • How can we connect? (API, Kafka, file extracts, direct database)

Extract and load

  • Full or incremental loads?
  • How much history do we need?
  • How big are the extracts?
  • How do we avoid slowing the source down?
  • What access or authentication is needed?

2. Create the tables

The table definitions (DDL — data definition language) mirror the source files exactly. Here's the players table:

CREATE TABLE IF NOT EXISTS bronze.players_details (
    player_id INTEGER,
    first_name TEXT,
    last_name TEXT,
    "name" TEXT,
    last_season INTEGER,
    current_club_id INTEGER,
    country_of_birth TEXT,
    country_of_citizenship TEXT,
    date_of_birth TEXT,
    "position" TEXT,
    sub_position TEXT,
    foot TEXT,
    height_in_cm INTEGER,
    contract_expiration_date TEXT,
    agent_name TEXT,
    international_caps INTEGER,
    international_goals INTEGER,
    market_value_in_eur NUMERIC,
    highest_market_value_in_eur NUMERIC
    -- ...plus a few more columns from the source file
);

3. Load the data

PostgreSQL can't load straight from a folder of files on its own, so I wrote a small Bash script. It empties each bronze table and reloads it from the matching CSV — a full load using truncate and insert.

DB_NAME="FootballDataWarehouse"
DB_USER="postgres"
DATA_PATH="./data"

echo "Loading bronze layer..."

psql -U $DB_USER -d $DB_NAME <<EOF
TRUNCATE TABLE bronze.players_details;
\copy bronze.players_details FROM '$DATA_PATH/players/players.csv' WITH (FORMAT CSV, HEADER true, DELIMITER ',');

TRUNCATE TABLE bronze.players_transfers;
\copy bronze.players_transfers FROM '$DATA_PATH/players/transfers.csv' WITH (FORMAT CSV, HEADER true, DELIMITER ',');

TRUNCATE TABLE bronze.games_details;
\copy bronze.games_details FROM '$DATA_PATH/games/games.csv' WITH (FORMAT CSV, HEADER true, DELIMITER ',');

-- ...and the same for the other bronze tables
EOF

4. Validate and document

To check completeness, I compared the number of rows in each table with the CSV files — simple SELECT queries were enough to confirm nothing was missing. Then I drew the data flow and committed the code.

Data flow from three source folders (players, games, competitions) into twelve bronze tables.
Bronze data flow: three source folders become twelve raw tables.

In the next part, the real work begins: exploring and cleaning the data in the silver layer.

See the code

Everything is on GitHub

All the SQL scripts, the load script and the diagrams for this project are in the repository.