Portfolio
Case study 04 / Data Warehouse

Case study 04 · Data engineering

Data Warehouse

Six raw CSV extracts from two unrelated business systems (a CRM and an ERP) turned into a single analysis-ready star schema in SQL Server. Built on a Bronze → Silver → Gold medallion architecture, where each layer has exactly one job and the layer below it is never modified.

Role
#learning_project
Warehouse
SQL Server
Sources
CRM + ERP · 6 CSVs
Model
Star schema · 1 fact, 2 dims

Learning project. This was my first time using SQL and Python inside a real project rather than for problem solving, so I followed a guided tutorial to get going and then extended it with my own analysis, visualisations and optimisations to take it past where the tutorial stopped.

3
Architecture layers
6
Source tables integrated
2
Quality-check suites
0
Transforms in the raw layer

01 · How it works

Two systems that were never designed to meet

A CRM knows customers, products and orders. An ERP knows the same customers under different keys, plus their country and product category. Neither is wrong and neither is complete, so the moment anyone wants revenue by category or by country they have to reconcile the two, and every analyst who needs that number reconciles it again, slightly differently. A warehouse is the decision to do that reconciliation once, write it down as code, and let everyone query the result.

01

Bronze, raw and untouched

Six CSVs bulk-loaded exactly as they arrive. No renaming, no casting, no filtering. Its only promise is that it faithfully reproduces the source, and it is never edited afterwards.

02

Silver, cleaned but same shape

Trimmed strings, coded values expanded into readable labels, types corrected, missing entries resolved. Same grain as Bronze, so this layer changes quality, not structure.

03

Gold, a star schema

Business-facing views joining CRM and ERP into one fact table and two conformed dimensions. "Conformed" meaning both systems finally agree on what a customer is, which is the entire point.

04

A passing test returns nothing

Two check suites written so that green means zero rows: no nulls in keys, no duplicates, no stray whitespace, no invalid dates. If a query returns rows, the layer does not get promoted.

The full pipeline

Each layer is its own set of DDL scripts, so any stage can be rebuilt without touching the others.

Source extracts
CRM: 3 files · ERP: 3 files
Bronze, raw landing
CSVs bulk-loaded into SQL Server exactly as they arrive. No renaming, no casting, no filtering, the raw layer's only promise is that it faithfully reproduces the source.
init_database.sql · ddl_bronze.sql · proc_load_bronze.sql
stored procedure · repeatable load
Silver, cleaned & standardised
Trimmed strings, expanded coded values into readable labels, resolved missing and inconsistent entries, corrected data types. Same grain as Bronze, this layer changes quality, not shape.
ddl_silver.sql
Quality gate, Silver
A check suite written so that a passing run returns zero rows. Nulls in keys, duplicates, stray whitespace, invalid date ranges. If anything returns rows, the layer isn't ready to promote.
tests/quality_checks_silver.sql
promote only on green
Gold, star schema
Business-facing views that join CRM and ERP into conformed dimensions and one fact table. Surrogate keys are generated here with ROW_NUMBER() so downstream models never depend on a source system's key.
ddl_gold.sql, 3 views
dim_customers
CRM identity + ERP demographics and country.
dim_products
CRM product master + ERP category tree.
fact_sales
Order grain, amount, quantity, price, dates.
Quality gate, Gold
Surrogate-key uniqueness, dimension coverage.

The Gold quality suite asserts that every surrogate key is unique and that no fact row points at a missing dimension, the two failures that quietly corrupt every downstream aggregate.

SQL analytics & reporting
Customer behaviour, product performance and sales trends, written against Gold, so a report is a SELECT with two joins instead of a nested reconciliation.
docs/data_catalog.md documents every field
Business insight

02 · Challenges

Where this got difficult

Challenge 01

The same customer, two different keys

The CRM identifies a customer one way, the ERP another. There is no lookup table that maps them, so joining the two systems is a rule I had to decide on, not a fact I could look up. Get it wrong and every number downstream is wrong in a way that still looks plausible. The important part was putting that rule in exactly one place in the Silver layer, versioned, instead of letting it be re-invented in each query.

Challenge 02

Deciding what Bronze is allowed to do

It is tempting to clean while loading. One trim, one cast, it saves a step. I made Bronze append-only and completely untransformed instead, which felt like extra work for no gain until the first time a Silver rule turned out to be wrong. Because the raw layer was still exactly what the source sent, the fix was a re-run. If I had cleaned on the way in, it would have been a data-recovery job with no clean copy to go back to.

Challenge 03

Writing tests that fail loudly

My first quality checks returned counts, and a count is easy to skim past. I rewrote them so a passing run returns zero rows, which means any output at all is a failure you cannot ignore. Nulls in keys, duplicate business keys, untrimmed values, dates outside valid ranges. Promotion to the next layer only happens on green.

03 · Limitations

What it doesn't do yet

Full reload, not incremental

Every run rebuilds from the CSVs. Fine at this size, wrong at production volume, where you would load only what changed.

No orchestration

Scripts run in order by hand. There is no scheduler, no retries and no dependency graph, so nothing recovers on its own.

No history tracking

Dimensions are overwritten. If a customer changes country, last year's orders silently move with them, because there are no slowly changing dimensions.

Files, not systems

Sources are CSV extracts, not live connections. Real integration brings schema drift and late-arriving data that this design has never had to survive.

Tests check shape, not meaning

The suites catch nulls, duplicates and bad types. They cannot tell whether a revenue figure is business-correct.

One fact table

The model answers sales questions. Anything else needs a new fact table and probably a new grain.

04 · What I learned

What I took away from building it

A layer is a promise, not a folder

Bronze, Silver and Gold only help because each one promises exactly one thing. The moment a layer starts doing two jobs, the boundary stops being useful and you are back to one big script.

Keep the raw copy, always

Refusing to clean on load felt wasteful right up until a rule was wrong. An untouched raw layer turns a bad decision into a re-run instead of a recovery.

Make green mean nothing

Tests that return counts get skimmed. Tests that return zero rows on success cannot be misread, and that small framing change is what makes the gate real.

Reconciliation is a decision, not a step

Choosing how two systems agree on a customer is a business call disguised as a join. Writing it down once, in code, is most of what a warehouse actually is.

READ THE scripts

All DDL, the load procedure, both quality-check suites and the data catalogue are in the repo, plus the data-flow, integration and model diagrams.