Home / Projects
DE · 06Live

SQL Data Warehouse on PostgreSQL

The classic warehouse pattern, built honestly: CRM + ERP extracts through bronze, silver and gold layers with PL/pgSQL procedures, into a Kimball star schema, guarded by 18 quality checks that stop the pipeline when they fail.

Data source6 CSV extracts from two simulated systems (CRM + ERP), 116k rows total
ArchitectureMedallion (bronze → silver → gold) → Kimball star schema
StoragePostgreSQL 16 in Docker, schemas as layers
StackPostgreSQLPL/pgSQLSQLDockerbash
StatusLive: 13 silver + 5 gold checks green, revenue reconciled across layers, one-command reproduction from a destroyed volume

Context

Before dbt and lakehouses, most analytics ran on exactly this: a relational warehouse loaded by SQL procedures. It is still everywhere, and the fundamentals it teaches (layered loading, conformed dimensions, surrogate keys, quality gates) transfer directly to any modern stack. This project builds that classic warehouse end-to-end in pure SQL, and proves every claim it makes with a reproducible run.

Business Problem

Sales data lives in two disconnected systems: a CRM (customers, products, transactions) and an ERP (demographics, locations, product categories). Nobody can answer "which product categories drive revenue, by customer segment and geography?" without manually stitching CSV exports. The warehouse integrates both sources into one dimensional model that answers those questions in a single query.

Architecture

CRM + ERP CSVs bronze · raw COPY silver · PL/pgSQL cleansing gold · star schema SQL analyses

Three PostgreSQL schemas in one database. Bronze is a faithful, auditable copy of the extracts: sales dates stay as raw 8-digit integers because raw is raw. Silver is where every cleansing rule lives, each commented with the data problem it fixes. Gold exposes dim_customers, dim_products and fact_sales as views, grain: one row per order line, declared before any SQL was written. Between the layers sit two quality gates that RAISE EXCEPTION on violation: the load is not "done" until they pass.

The Source Data Is Deliberately Dirty

Challenges

Rebuilt from the rule, not transcribed from the code

The warehouse design comes from a well-known T-SQL project (credited below). Every procedure here was rewritten in PostgreSQL from the cleansing rule it implements: BULK INSERT became a data-driven COPY loop in dynamic SQL, GETDATE() became now(), integer-date casts became to_date(int::text,'YYYYMMDD'). Translating dialect by patching syntax until it runs teaches nothing; translating from the rule proves you understood it.

A gate that cannot fail is not a gate

Inspection queries that SELECT problems and return zero rows prove little: nobody reads them on the hundredth run. Every check here raises an exception, and the runner uses ON_ERROR_STOP, so a violation halts the pipeline. Proven by sabotage: injecting one duplicate customer id aborts the gate with ERROR: S01 FAIL: 1 duplicate/null cst_id.

The shell miscounted; the database didn't

Validating bronze against the CSVs, wc -l reported one row fewer than COPY loaded, on exactly the files with no trailing newline. The database was right. Lesson: even your verification needs verification, and row counts belong to query output, not to eyeballed console lines.

Idempotency is proven, not assumed

Every load is truncate-and-load and every DDL drops before creating. Claim tested two ways: the full pipeline run twice yields identical counts on all 12 tables, and a clean-room rebuild after docker compose down -v ends with both gates green.

Results

Row lineage: nothing lost by accident, everything lost by rule.

StageRowsWhy
bronze.crm_cust_info18,494= CSV data rows, verified per file
gold.dim_customers18,48410 duplicate/null ids removed by rule
bronze.crm_prd_info397all product versions
gold.dim_products295current versions only
gold.fact_sales60,398grain preserved 1:1 from silver (gate G04)

18 quality checks green, including revenue reconciliation across layers: gold Σsales_amount = silver Σsls_sales = 29,356,250. And the warehouse answers real questions: Bikes are 96.5% of revenue while Accessories move 2× the units; the US leads total revenue (9.16M), but Australia monetizes each customer 2× better (2,523 vs 1,224); 2013 alone holds more than half of all revenue.

Tech Stack

CategoryTool
DatabasePostgreSQL 16 (Docker Compose)
ETLSQL + PL/pgSQL stored procedures
Loadingserver-side COPY
Runnerbash + psql, ON_ERROR_STOP, one command
ModelingKimball star schema over medallion layers

Architecture and source datasets from the SQL Data Warehouse Project by Baraa Khatib Salkini (MIT). This is an independent rebuild on PostgreSQL (own code, own gates, own decisions), not a fork.

View the code on GitHub ↗

← Previous
Modern Data Stack ELT