◆ Infrastructure layer · flagship

how-to-schema-and-warehouse

The layer everyone fakes with a tidy ERD of already-clean data. This one starts from a real operational mess - four source systems, mismatched keys, dirty labels - and ends at a DuckDB star schema you can actually query. Every number below came from a real build: seed 42, learn-python env, 107,841 raw transactions modeled.

raw dumplakewarehousemartsagent (layer 4)
Case: Everrest · B2B2C retail platform seed = 42 DuckDB · pandera · matplotlib star schema: 1 fact + 4 dims contract: PASS
01
Step 1

Input - four source systems, none of them tidy

A real data platform is not handed clean tables - it is handed exports from whatever systems happened to produce them. Everrest arrives as four raw dumps with their own key formats, their own casing, and their own defects. The job: turn this into one warehouse the whole company can trust and query.

raw_transactions.csv 107,841 rows
  • txn_id · one row per order line
  • order_ts_utc · timestamp
  • status · mixed casing (Delivered/delivered)
  • customer_id · some orphaned
  • merchant_id · "M0001" format
  • category_raw · dirty labels
  • product_id · some orphaned
raw_products_export.csv 5,000 rows
  • product_id · catalog master
  • merchant_id · fk
  • catalog_price · current price
  • unit_price on txns drifts from this
raw_merchants_extract.json 400 records
  • merchant_key · "MER-0001" format
  • legacy_id · "M0001" (the bridge)
  • category · dirty labels too
  • tier · standard/premium/enterprise
  • onboarded · date string
raw_payments_export.csv 46,007 rows
  • pmt_ref · processor ref
  • order_id · fk
  • method · card/wallet/bnpl/cod
  • amount_reported · fabricated for M0333
  • paid_at · PH logged in local time
02
Step 2

Generate sample data - defects planted in the RAW layer

The quirks are ingestion problems, so they live in the raw dump - which is exactly why the warehouse build has real work to do. A seeded generator produces the four sources; each defect is documented in its docstring, turning Step 5 into a recall test: did the model resolve everything the raw layer broke?

generate_raw.py ↗ · excerpt
# Quirk 3: the merchant master uses a different key format than transactions.
ext["merchant_key"] = ext.merchant_id.str.replace("M", "MER-")   # M0001 -> MER-0001

# Quirk 4: PH payments logged in local time -> paid_at lands 8h BEFORE the order.
pay.loc[ph, "paid_at"] = pay.loc[ph, "paid_at"] - pd.Timedelta(hours=8)

# Quirk 6: 800 customers cloned under a new id, identical signup/channel/region.
clones = customers.sample(800, random_state=SEED).copy()
clones["customer_id"] = [f"C9{i:05d}" for i in range(1, len(clones) + 1)]
🔗 Orphan foreign keys2.5% of line items and 1% of orders point to ids in no master
🔤 Dirty categoricals8 real categories arrive as 14 label variants
🗝 Heterogeneous keysmerchant master uses MER-0001, transactions use M0001
🕗 Timezone bugPH payments logged 8h before their order
🔢 Round-number fabricationM0333 reports only round-hundred amounts
👥 Duplicate identities800 customers cloned under a second id
03
Step 3

Objective - a trust question, not a diagram

Anyone can draw an ERD. The real question an infrastructure lead has to answer is whether the platform can be built on at all.

Can we turn this raw operational dump into a warehouse the whole company can query and trust - and what silently breaks if we skip the modeling?
  • Do the foreign keys resolve, or are we joining onto ghosts?
  • Can the four source systems even be joined - do the keys reconcile?
  • Are the entities clean - categories, customers - or silently split and duplicated?
  • Once modeled, does one clean query return the right answer the raw dump could not?
  • Can we enforce all of this with a contract so it never regresses?
04
Step 4

Find-skills - the modeling toolbox

Before writing DDL, assemble the tools that serve this objective: an engine that queries files directly, a contract library that turns integrity rules into code, and a promotion path to permanent CI gates.

OSS engine

DuckDB

Query CSV/JSON/Parquet directly and build the star schema in-process - the whole warehouse is one SQL file, zero infrastructure.

OSS tool

pandera

Schema + referential-integrity assertions as code - "product_id must exist in dim_product" becomes a test the build enforces.

OSS tool

Great Expectations

Promotes the data contract to a permanent CI gate - the orphan keys and dirty labels never ship twice.

OSS tool

DataHub / OpenMetadata

Catalog + lineage: register the dims and fact so downstream teams discover the modeled tables, not the raw dump.

Pattern

Kimball star schema

One fact at a clear grain + conformed dimensions - the model that makes every downstream query a simple join.

Skill

dataviz discipline

One anomaly color (amber) across every reconciliation chart; row counts always shown so nothing hides.

05
Step 5

Build - raw dump to queryable star schema

One SQL file does the whole transform: stage the four sources, reconcile the keys, normalize the labels, dedupe identities, fix the timezone, then assemble one fact and four conformed dimensions. Grab the real code below.

The star schema

Star schema ERD: fact_orders at the center connected to dim_merchant, dim_customer, dim_product and dim_date

One fact at a clear grain (one order line) surrounded by four conformed dimensions. Every downstream question is now a simple join instead of a wrangling exercise.

warehouse.sql ↗ · the cleaning happens in the transform
-- Quirk 3: reconcile 'MER-0001' -> 'M0001' via legacy_id.
-- Quirk 2: normalize 14 dirty labels -> 8 canonical categories.
CREATE OR REPLACE TABLE dim_merchant AS
SELECT legacy_id AS merchant_id, clean_category(category) AS category, tier
FROM stg_merchants;

-- Quirk 6: collapse duplicate identities on a (signup, channel, region) fingerprint.
SELECT min(customer_id) OVER (PARTITION BY signup_ts, channel, region) AS customer_id

-- Fact: orphan keys are FLAGGED, not silently dropped.
(dp.product_id IS NULL) AS is_orphan_product

Prove it's queryable: the same question, before and after

The whole point of modeling. On the raw dump, "revenue by category" splits into 14 mislabeled buckets. On the star schema, one clean join returns 8 whole categories.

Query result on raw labels: 14 category buckets including Grocary and two Beauty rows
Query result on the star schema: 8 clean category buckets
Star schema join result: top merchants by revenue with M0007 highlighted

A real star-schema join (fact_orders x dim_merchant): top merchants by revenue - and the M0007 bulk-wholesale outlier surfaces immediately, correctly attributed.

What the model fixed

Row-count funnel from raw dump to modeled clean fact
Nothing lost. Every one of 107,841 transactions is accounted for through the lineage; orphans are flagged, not silently dropped.
Category cardinality 14 to 8
14 → 8. Dirty label variants normalized in dim_merchant, so every category rollup is whole.
Orphan foreign key rates flagged
Keys checked. 2.5% of line items and 1% of orders are orphaned - quarantined with a flag the contract can enforce.
Duplicate customer identities collapsed
People, not rows. Duplicate identities collapsed on a signup/channel/region fingerprint so acquisition counts are honest.
Payment latency before and after timezone fix
Time made sane. PH payments no longer land 8 hours before their order once paid_at is shifted back to UTC.
Reported versus modeled payment gap by merchant, M0333 highlighted
Reconciliation catches fraud. M0333's reported payments diverge sharply from its modeled order value - a fabrication the warehouse surfaces.
data_contract.yaml ↗ · enforced by pandera, promotable to a CI gate
product_id:
  foreign_key: dim_product.product_id   # enforced for non-orphan rows
checks:
  - name: referential_integrity_product
    rule: "every non-orphan row has product_id in dim_product"
  - name: no_negative_payment_latency
    rule: "paid_ts_utc >= order_ts for all rows"
  - name: category_domain
    rule: "dim_merchant.category in the 8 canonical categories"
# build result: contract PASS - 0 referential-integrity violations
Planted defect (in raw)Resolved byResultStatus
Orphan foreign keysfact flags + contract2,696 + 1,074 rows quarantined✓ resolved
Dirty category labelsclean_category() in dim_merchant14 → 8 categories✓ resolved
Heterogeneous merchant keyslegacy_id reconciliationMER-#### → M####✓ resolved
Timezone bug (PH)+8h shift in clean_payments0 negative latencies✓ resolved
Round-number fabricationreported vs modeled reconciliationM0333 flagged✓ resolved
Duplicate identitiesfingerprint dedup in dim_customer662 collapsed✓ resolved
06
Step 6

Expert review - a check that changes the model

A panel of five senior reviewer agents - each with 10+ years in data engineering, architecture, and business - tore into the naive first pass (naive_query.py, kept in the repo) that just queried the raw dump. It ran clean and returned plausible numbers, which is what made it dangerous. Every fix below is in the warehouse above.

Principal Data Architect
15 yrs · warehouse modeling

"You queried the raw dump directly and joined merchant on merchant_id. The master uses MER-0001 keys - your join matched zero rows and you never noticed. Model conformed dimensions with a reconciled key."

Fix applied: dim_merchant reconciles legacy_id; the star join now resolves every merchant.
Senior Data Engineer
13 yrs · pipeline integrity

"No referential-integrity check at all. 2.5% of line items point at products that do not exist - they vanish into a join and skew every total."

Fix applied: orphan keys flagged on the fact (not dropped) and enforced by the pandera contract.
Analytics Methodology Lead
11 yrs · data quality

"Revenue-by-category on the raw label gives you 'Beauty' twice and a 'Grocary'. Every rollup is silently split until you normalize on ingest."

Fix applied: clean_category() collapses 14 labels to 8 inside dim_merchant, before any query.
Data Governance / Contract Lead
12 yrs · trust & lineage

"Even if you fix it once, nothing stops it regressing. Where is the contract? And PH payments dated before their order should have failed a check immediately."

Fix applied: data_contract.yaml with FK, category-domain and no-negative-latency checks; timezone corrected in clean_payments.
Commercial / Business Lead
14 yrs · impact

"Your naive total counts a fabricated merchant and duplicate customers. That number goes to the board and it is wrong."

Fix applied: reported-vs-modeled reconciliation flags M0333; duplicate identities collapsed so counts are honest.
before → after · the join that silently returned nothing
-- v1 (before): query the raw dump, join on merchant_id
SELECT count(*) FROM t JOIN m ON t.merchant_id = m.merchant_key;
--   -> 0 rows. MER-0001 never equals M0001. Silent, catastrophic.

-- v2 (after): reconcile the key in dim_merchant, then join the star
SELECT dm.category, sum(f.net_amount)
FROM fact_orders f JOIN dim_merchant dm USING (merchant_id);   -- every row resolves

Run this skill on your own sources

Install once, then point it at your raw exports - the same 6 steps build a star schema and a contract on your data (step 2 is skipped when real data exists).

terminal
/plugin marketplace add phoebefu6/phoebe-data-skills
/plugin install how-to-schema-and-warehouse@phoebe-data-skills