Dimensional Modeling
Dimensional Modeling
M ETHODOLOGY EDITION
Dimensional
Modeling
◆
10 53 113 400+
CHAPTERS PATTERNS REAL-WORLD REFERENCE
EXAMPLES LINKS
Table of Contents
Fact Tables
01
Transaction · Periodic Snapshot · Accumulating Snapshot · Factless Fact
Dimension Types
02
Conformed · Degenerate · Junk · Role-Playing · Outrigger · Bridge · Inferred Member
SCD Types 0 – 7
03
Retain Original · Overwrite · Add Row · Add Column · History Table · Mini-Dim · Hybrid · Dual-Key
RCD Patterns
04
Mini-Dimension · Push to Snapshot · Junk Dim for Fast Flags
Bi-Temporal Modeling
05
Valid Time · Transaction Time · Four-Quadrant Query Model
Schema Patterns
06
Star · Snowflake · Galaxy · Data Vault 2.0 · One Big Table (OBT)
Hierarchy Patterns
07
Fixed-Depth · Ragged · Recursive / Parent-Child · Closure Table
Grain Concepts
08
Atomic Grain · Aggregate Grain · Grain Conflict — the #1 mistake
Key Strategies
09
Surrogate · Natural · Durable Supernatural · Hash Key (SHA-256)
Tables
The central table in a star schema. Stores measurable, quantitative data about a business
process. Every fact table must declare a precise grain — one row represents exactly one
thing.
Description: One row per atomic business event. Each row represents a single discrete
transaction — a sale, a click, a payment, a login. Rows are never updated; new events
produce new rows. This is the most granular and most common fact table type.
🛒
Use Case: Retail SalesA supermarket chain captures every product scan at checkout.
Each line item (product × transaction) is one row. Analysts can then roll up to daily/
weekly sales, compare product performance, or join to customer and store dimensions.
SALE_KEY (PK) DATE_KEY (FK) PRODUCT_KEY (FK) STORE_KEY (FK) CUSTOMER_KEY (FK) QUANTITY UN
SCHEMA DIAGRAM
DimDate
date_key PK
full_date
day_name
month
quarter
FactSales
DimCustomer
customer_key PK
customer_name
segment
city
RETAIL BANKING
Grain: one row per sale line item · ~500M Grain: one row per card transaction · ~2B
rows/year for a large chain rows/year for a large issuer
E-COMMERCE
✦ Captures every atomic event — no information ✔ Business needs to analyze individual events —
lost at ingestion sales, clicks, payments, logins
✦ Can answer any question at any grain through ✔ Analysts require drill-through to the transaction
GROUP BY rollups level from any summary
✦ Supports drill-through from summary ✔ Source system produces discrete event records
dashboards to individual transactions (POS, order systems, web logs)
✦ Append-only pattern enables simple, reliable ✔ Ad-hoc queries are unpredictable — you can't
incremental ETL/ELT pre-define the rollup level
✦ Foundation for all higher-level aggregates and ✔ This is always the recommended starting point —
summary tables build atomic grain first
Description: One row per entity per time period, regardless of activity. The row is appended
at each period-end (daily, weekly, monthly) capturing the state at that point in time. Even if
nothing changed, a row is still inserted. Ideal for tracking balances, levels, and statuses over
time.
🏦
Use Case: Bank Account BalancesA bank captures every account's closing balance
at end of day. Even if a customer made no transactions today, a row is inserted with
yesterday's balance. This allows analysts to chart balance trends, compute averages,
and identify dormant accounts.
Key insight: Row for 2024-03-02, ACC-1001 has zero transactions but is still present.
This is what distinguishes periodic snapshot from transaction fact — completeness of
the time series.
✓ Complete time series — no gaps ✓ Easy trend analysis ✗ Grows even with no
activity ✗ Not suitable for event-level drill-down
⇗ dbt Snapshots
HR / WORKFORCE
✦ Guarantees a complete, gapless time series — ✔ Business metric is a level or balance (inventory,
even periods with zero activity are represented headcount, account balance) not an event
✦ Point-in-time balance/level lookups require no ✔ Need to answer "what was the state on date X?"
complex window functions without transaction-level scanning
✦ Decouples reporting from source system query ✔ Source system measures change continuously
load — data is pre-snapshotted and only the period-end value matters
Description: One row per entity tracking it through a multi-step pipeline lifecycle. Unlike
other fact tables, rows are updated as the entity progresses through stages. Multiple date
FKs represent each milestone. Lag metrics (days between stages) are computed directly in
the row.
📦
Use Case: Order Fulfillment PipelineAn e-commerce platform needs to track each
order from placement through delivery. A single row per order is updated as each
milestone is reached. This lets analysts compute average fulfillment cycle times,
identify bottlenecks, and measure SLA compliance.
E-COMMERCE INSURANCE
Grain: one row per order (updated at each Grain: one row per insurance claim ·
milestone) · Rows updated ~5–7 times per Claims remain open (active rows) for
order lifecycle weeks to months
HEALTHCARE
✦ Computes end-to-end cycle time and stage-to- ✔ Business process has a well-defined, ordered set
stage lag directly in a single row of stages (order → ship → deliver)
✦ Identifies pipeline bottlenecks — which stage ✔ Management needs cycle time and lag metrics
consistently adds the most delay across pipeline stages
✦ SLA monitoring is straightforward: compare ✔ Each entity (order, claim, ticket) has exactly one
milestone dates against targets row through its entire lifecycle
✦ Current status of every in-flight entity is always ✔ Pipeline has a definitive start and end — not an
queryable with a simple filter open-ended continuous process
Description: A fact table with no numeric measures — only foreign keys to dimension tables.
Used to record that an event occurred or that a relationship exists. The "fact" is the
occurrence of the event itself. COUNT(*) is the primary aggregation.
🎓
Use Case: Student Course AttendanceA university wants to track which students
attended which lectures on which date. There's no numeric measure — the presence of
the row is the fact. Analysts can answer: "How many students attended on Tuesday?" or
"Which lectures had < 50% attendance?"
✓ Records events and coverage elegantly ✓ Enables "what didn't happen" queries ✗
COUNT(*) only — limited analytics
Grain: one row per product × store × Grain: one row per student × session
promotion × day · Used exclusively for attended · Absence = no row (or compare
coverage vs. actuals comparison to FactScheduledClasses coverage table)
HR
⇗ Manufacturing HR Analytics
✦ Models real-world events that have no numeric ✔ The business event has no meaningful numeric
measurement naturally and correctly measure — presence alone is the fact
✦ Enables "what didn't happen" queries — a ✔ Need to track attendance, eligibility, enrollment,
capability no other fact type supports directly or coverage relationships
✦ Coverage variant is the only way to compute ✔ Analysis requires identifying absence — products
promotion/eligibility effectiveness with no sales, students who missed class
✦ Very compact — only FK columns, no measures ✔ Modelling a many-to-many relationship that also
consuming storage carries a date or context dimension
Description: Every measure in a fact table must be classified by its additive behaviour —
whether it can be meaningfully summed across all dimensions (additive), only some
dimensions (semi-additive), or no dimensions at all (non-additive). This classification drives
which aggregation functions are valid and what errors to prevent in reports.
For non-additive facts: Never store margin_pct directly in the fact table for
aggregation. Store revenue and cost as additive facts, then compute margin_pct =
(revenue - cost) / revenue at query time on aggregated components.
Description: Fact values themselves can change after being recorded — financial
restatements, error corrections, retroactive pricing adjustments, or revised measurements.
Unlike dimensions (which use SCD patterns), fact tables are typically append-only, so changed
fact values require a specific correction strategy to avoid corrupting historical analysis.
📋
Use Case: Financial RestatementA company discovers a revenue recognition error in
Q1. The original Q1 revenue of $450,000 must be restated to $420,000. The correction
must be recorded without destroying the audit trail of the original figure.
✦ Full audit trail — the original value and the ✔ Fact values are subject to correction,
correction are both permanently visible restatement, or retroactive adjustment after initial
load
✦ Append-only — the fact table never needs
UPDATE operations ✔ Regulated industries (finance, healthcare) where
an audit trail of the original recording is mandatory
✦ SUM always produces the net correct value
automatically ✔ Source systems issue correction transactions
(credit notes, reversal postings) naturally
✦ Load_date on each row enables "as-of" queries
to reconstruct any prior view ✔ Never overwrite fact rows unless there is zero
audit or regulatory obligation whatsoever
Description: Facts that arrive in the data warehouse after their event date — because of
batch delays, offline transactions, manual reporting, or system outages. A sale made on
March 1st may not be loaded until March 8th. This creates "holes" in periodic snapshots and
causes previously published reports to change when late data lands.
🕐
Use Case: Field Sales Rep Offline OrdersSales reps capture orders offline on tablets
in remote areas. When they reconnect on March 5th, orders from March 1–4 all arrive
together. The March 1–4 periodic snapshots are already published and will need
reprocessing.
Transaction Fact Low — just append the late Append with correct date_key;
row with correct event date add load_date for lineage
Periodic Snapshot High — prior period snapshots Reprocess affected periods; use
must be reprocessed a "restated" flag or version
column
Accumulating Medium — update the correct Update the row with the late
Snapshot row's milestone dates milestone; capture actual vs
load timestamp
Description: Facts (measures) that are defined identically — same business logic, same
formula, same grain — across multiple fact tables and data marts. Just as conformed
dimensions enable consistent entity identification across marts, conformed facts enable
consistent metric definitions. "Revenue" means exactly the same thing in FactSales,
FactReturns, and FactBudget.
⚖️
Use Case: Revenue Definition"Revenue" in the Sales mart excludes tax and refunds.
If the Finance mart defines "Revenue" to include tax, a drill-across comparison will
produce an unexplained discrepancy. Conformed facts prevent this by mandating a
single definition in the enterprise data dictionary.
Rule: If the same measure name appears in two fact tables with different formulas,
one of them must be renamed. "Revenue (ex-tax)" and "Revenue (inc-tax)" are two
different facts — they must never share the same name.
✦ Drill-across queries produce consistent, ✔ The same KPI name appears in more than one
trustworthy numbers across all marts data mart or fact table
✦ BI tools and semantic layers can safely expose ✔ A data governance or centre of excellence team
the same metric from multiple fact tables can own and enforce metric definitions
✦ Reduces the most common source of data trust ✔ If definitions differ legitimately — rename them
erosion in enterprise analytics explicitly; never reuse the same name for different
formulas
Types
Dimensions provide the context for facts. They answer Who, What, Where, When, Why, and
How. Different dimension archetypes address different structural challenges in your data
model.
Description: A dimension that is shared identically (same columns, same grain, same values)
across multiple fact tables and data marts. Conformed dimensions enable "drilling across" —
joining results from two different fact tables using the shared dimension as a common spine.
🏢
Use Case: Enterprise DimDateA single DimDate table used by FactSales,
FactInventory, FactHR, and FactFinance. Because they all share the same date
dimension, analysts can compare sales revenue vs. headcount on the same date axis
without any join ambiguity.
DimDate (CONFORMED)
date_key PK
full_date
day_of_week
month_name
quarter
fiscal_year
is_holiday
Tesco / Reliance Retail DimDate & Axis Bank / Yes Bank DimCustomer
DimProduct DimCustomer is shared across
DimDate is loaded once and used by FactCardTransactions, FactLoanRepayments,
FactSales, FactInventory, FactMarketing, FactSavingsSnapshots, and
FactHR — all 12 fact tables share the exact FactMobileBankingEvents. "Total customer
same date dimension. "Sales revenue by exposure" drill-across query works because all
month" and "Inventory level by month" use four fact tables join to the same customer_key
the same month_name from the same with identical customer segment definitions.
DimDate row — guaranteed consistent
reporting across all marts. Conformed customer_segment definition
prevents "Premium" in one mart vs "High
Without DimDate conformance: Sales shows Value" in another for the same customer
"Jan 2024", Inventory shows "January
2024" — BI tool treats them as different
⇗ Kimball Conformed Dimensions
values in drill-across
✦ Enables drill-across queries — comparing ✔ Multiple fact tables or data marts need to share
metrics from two different fact tables on the the same dimension (Date, Customer, Product)
same axis
✔ Business requires cross-process analysis — e.g.
✦ Eliminates inconsistent definitions of common compare sales vs. returns on the same product axis
entities like Date, Customer, Product across
✔ Enterprise DW with multiple subject areas
teams
managed by different teams
✦ Reduces redundant ETL work — one shared
✔ A "centre of excellence" or data governance
dimension loaded once, reused everywhere
team can own and maintain the shared dimension
✦ Enforces a common business vocabulary and
consistent hierarchies across the enterprise
Description: A dimension attribute that lives directly in the fact table with no corresponding
dimension table. It acts as a grouping/filtering key but has no additional descriptive attributes
to justify a separate table. Common for operational transaction identifiers.
🧾
Use Case: Invoice NumberAn invoice number groups multiple line items (rows) in a
fact table. It's a meaningful identifier you filter and group by, but there are no additional
attributes (invoice date, customer etc.) that aren't already captured by other FK
dimensions. So it stays in the fact table as a degenerate dimension.
INV-9920 is the degenerate dimension. It groups two line items into one invoice.
No separate DimInvoice table exists — there's nothing more to say about the invoice
itself beyond what's already in the fact row.
✦ Eliminates an unnecessary dimension table — ✔ The identifier (invoice#, order#, ticket#) has no
reduces join count and model complexity additional descriptive attributes of its own
✦ Transaction grouping is available directly in the ✔ All attributes of the grouping entity are already
fact table without a join captured by other FK dimensions
✦ Preserves meaningful operational identifiers ✔ The identifier is used only for grouping/filtering,
(invoice, order number) for traceability not for dimensional analysis
✦ No surrogate key overhead — the natural key ✔ Creating a full dimension table would result in a
lives directly in the fact row 1:1 mapping to the fact with no added value
🏷️
Use Case: Order FlagsAn order fact table would otherwise have: is_rush_order (Y/N),
is_promotional (Y/N), payment_type (Card/Cash/Credit), channel (Web/App/Store).
Instead of 4 columns in the fact, pre-build all 2×2×3×3=36 possible combinations in
DimOrderFlags.
4 Y Y Cash Store
Result: 4 columns collapsed into 1 FK. Ju
… … … … …
dim never grows beyond total combinatio
its attributes.
Junk dim reduces 5 fact columns to 1 FK; Used in regulatory RBI reporting to
flag combination count is bounded and segment transactions by type combinations
static once defined without runtime flag evaluation
✦ Collapses many low-value columns in the fact ✔ Fact table has multiple low-cardinality flag/
table into a single clean FK indicator columns (Y/N, 3–4 enum values each)
✦ Pre-built combinations mean the junk dim is ✔ Total distinct combinations across all flags is
effectively static — no ongoing growth manageable (under a few thousand rows)
✦ Consolidates filtering and segmentation logic ✔ No individual flag justifies its own full dimension
into one queryable dimension table
✦ Avoids nullable columns and sparse boolean ✔ Flags frequently appear together in filter
flags scattered across the fact table conditions or segmentation logic
Description: A single physical dimension table that is referenced multiple times in the same
fact table, each time under a different alias (role). The most common example is DimDate
being used as OrderDate, ShipDate, and DeliveryDate simultaneously.
📅
Use Case: Multi-Date Order FactAn order has three meaningful dates: when it was
placed, when it was shipped, and when it was delivered. All three reference the exact
same DimDate table, just aliased differently. One physical table, three logical roles.
FactOrders
DimDate (as ShipDate)
order_date_key FK
ship_date_key FK
→
deliver_date_key FK
DimDate (as DeliverDate)
✦ One physical table maintained instead of three ✔ A single fact table has multiple FKs to the same
copies — no sync risk or duplication dimension type (e.g. multiple date milestones)
✦ Changes to the dimension schema propagate to ✔ The same dimension appears in multiple
all roles automatically semantic roles across the model
✦ Analysts can filter by any date role ✔ Creating separate physical copies would lead to
independently without schema proliferation identical structures maintained in parallel
✦ Reduces storage and ETL maintenance to a ✔ BI tool supports aliasing — most modern tools
single dimension load process (Tableau, Power BI, Looker) handle this natively
🗺️
Use Case: Store → GeographyDimStore references DimGeography for city, state,
country, region. Multiple stores share the same geography entries. DimGeography is
never directly FKed from the fact — it's only reachable via DimStore. Fact → DimStore →
DimGeography.
DimStore
FactSales store_key PK
store_key FK
→ store_name
→
geo_key FK →
DimGeography (Outrigger)
geo_key PK
city
state
country
region
Warning: Outrigger dimensions introduce snowflaking — they add join complexity. Use
sparingly. If DimGeography is only for DimStore, it may be better to simply denormalize
city/state into DimStore itself.
RETAIL MANUFACTURING
✦ Allows a rich sub-dimension to be shared across ✔ Multiple primary dimensions reference the same
multiple primary dimensions secondary dimension (e.g. Store and Warehouse
both reference Geography)
✦ Keeps the primary dimension lean by
externalizing reusable reference data ✔ The sub-dimension has enough attributes to
justify its own table — not just 1–2 columns
✦ Changes to the shared sub-dimension (e.g.
region boundaries) update in one place ✔ Performance is acceptable — the extra join hop is
tolerable for the query workload
👨👩👧👦
Use Case: Customer → Multiple AccountsA banking customer can own multiple
accounts (savings, checking, mortgage). FactTransactions references customer_key, but
the M:M relationship to accounts requires a bridge table to avoid double-counting.
Weighting factor: When a fact (e.g., shared account transaction) must be attributed
to multiple customers, the weight column (0.5 each for Alice's two accounts) prevents
double-counting in aggregations.
BANKING HR
✦ Weighting factor enables proportional ✔ Direct FK in the fact would cause row duplication
attribution — prevents double-counting in shared and double-counting in aggregations
scenarios
✔ Attribution/weighting across the M:M members is
✦ Clean separation of the relationship from both required for correct metric calculation
the fact and dimension tables
✔ Examples: customer multi-account banking,
✦ Relationship membership can be versioned or employee multi-skill HR, product multi-category
dated independently of either entity catalog
Description: When a fact record arrives before its corresponding dimension record, a
placeholder "inferred member" row is inserted into the dimension with a generated surrogate
key and NULLs for all descriptive attributes. The fact row is linked to this placeholder. When
the actual dimension data arrives, the placeholder is updated (backfilled) with real values.
⏱️
Use Case: New Employee Sales on Day 1A sales transaction arrives for new
employee EMP-9042, but HR hasn't processed the employee record into DimEmployee
yet. Rather than drop the fact or halt the pipeline, an inferred member row is created
and later backfilled.
✓ No fact data lost during pipeline delays ✓ Pipeline keeps running ✗ NULLs in
reports until backfill completes ✗ Requires backfill reconciliation logic
E-COMMERCE HEALTHCARE
Meesho / Snapdeal New Seller First AIIMS / Fortis New Patient Walk-In
Listing A patient walks in for emergency treatment
A new seller lists a product at 11:58 PM. At before their registration is complete in the
midnight, the nightly ETL loads hospital system. FactTreatmentEvent is
FactProductView events — including 200 views recorded with an inferred DimPatient
of the new listing. DimSeller doesn't yet have placeholder (age, name = NULL). Once the
the seller (onboarding ETL runs at 6 AM). An patient is registered and the record syncs to
inferred member SK is assigned; 200 fact rows the DW, the placeholder is backfilled.
are safely linked. At 6 AM, DimSeller backfills Treatment cost is never lost.
the placeholder row with real seller data.
Particularly critical for trauma/
is_inferred = TRUE rows appear as emergency cases where treatment starts
"Pending Seller" in dashboards; KPI before administrative registration
dashboards show total views correctly completes
even before backfill
✦ Zero fact data loss — no transactions are held ✔ Source systems feeding dimensions are known to
back or discarded due to dimension lag lag behind transactional systems
✦ Pipeline remains operational and on-schedule ✔ Pipeline SLA requires zero fact data loss even
even when source systems are out of sync when dimension data is delayed
✦ Referential integrity is preserved from day one ✔ The reporting team accepts a short window of
via the placeholder surrogate key NULL attributes in affected rows
Description: A conformed dimension that contains only a subset of rows or columns from the
full base dimension — scoped to a specific data mart, product line, or aggregation level. A
shrunken dimension must be a perfect subset of the full conformed dimension to maintain
drill-across compatibility. Used alongside aggregate fact tables.
🔬
Use Case: Monthly Product Summary MartA full DimProduct has 50,000 SKUs with
30 attributes. A monthly summary fact table only needs product at the brand level with
5 attributes. DimProductBrand is a shrunken subset — 400 rows, 5 columns —
conformed to the full DimProduct.
RETAIL BANKING
✦ Enables aggregate fact tables at higher ✔ Building aggregate fact tables that operate at a
granularity without forcing the full base higher grain than the atomic base fact table
dimension
✔ A specific mart or report only needs a subset of
✦ Smaller dimension improves query performance the full dimension's rows or columns
in aggregate marts dramatically
✔ Performance optimisation is needed and the full
✦ Maintains drill-across compatibility because dimension contains far more detail than required
values are a strict subset of the conformed base
✔ Must always be derived from and stay conformed
dim
to the full base dimension — never designed
✦ Reduces the number of attributes BI users are independently
exposed to in summary-level reports
Description: A dimension that is pre-loaded at DW build time with a fixed set of known
values and never (or very rarely) sourced from an operational system. Contains reference
data like status codes, priority levels, day-of-week names, flag descriptions, or ISO codes. The
DW team owns and maintains it directly — not a downstream of any source system extract.
📖
Use Case: DimDayOfWeek7 rows, pre-loaded once, never changes: Monday through
Sunday with attributes like is_weekend, sort_order, abbreviation. No source system
feeds this — the DW team creates it at setup and it never needs ETL refreshes.
1 Monday Mon 1 N
2 Tuesday Tue 2 N
6 Saturday Sat 6 Y
7 Sunday Sun 7 Y
🏷️ 🌐 ⭐
DimOrderStatusNew, DimCurrencyISO 4217 DimPriorityLow /
Processing, Shipped, currency codes, names, Medium / High / Critical
Delivered, Cancelled — and symbols — stable — sort order + colour
fixed set of known reference table codes for dashboards
states
Pre-loaded via a seed file in dbt; stored is_final_state flag enables "open
in a "reference" schema; excluded from transactions" queries without hardcoding
daily ETL monitoring status codes in every SQL filter
✦ No ETL pipeline needed — zero ongoing ✔ Reference values are small, well-known, and
operational cost once loaded stable (status codes, priority levels, day names)
✦ Adds descriptive labels and sort orders to coded ✔ No source system owns a clean, queryable
values from source systems version of the reference data
✦ Enables clean grouping and filtering without ✔ The DW needs to enrich coded values (e.g. status
relying on source system code tables = "3") with human-readable labels
✦ DW team has full control — no source system ✔ Add sort_order and display_name columns to
dependency or refresh scheduling required enable correct dashboard ordering beyond
alphabetical
Description: NULL foreign keys in fact tables cause rows to be silently excluded from GROUP
BY aggregations — a query joining FactSales to DimCustomer will drop all fact rows where
customer_key IS NULL. The Kimball solution is to never allow NULL FKs in fact tables. Instead,
dimension tables always contain a special "Unknown" or "N/A" row with surrogate key = 0 (or
-1). All unresolvable fact FKs point to this row.
⚠️
The Null DangerAn e-commerce site has 50,000 transactions. 3,000 are from
anonymous (guest) customers with no customer record. If customer_key = NULL for
these rows, all 3,000 transactions are silently dropped from every report that joins
DimCustomer. Revenue is understated by an unknown amount.
understated by $67.50
S-002 0 → "Unknown" 45.00
Kimball Rule: Dimension tables must always contain a row for "Unknown", "N/A", "Not
Applicable", or "Not Yet Assigned" with a well-known surrogate key (0 or -1). Fact table
FK columns must be declared NOT NULL — the ETL must resolve every unmatched FK
to the Unknown row.
E-COMMERCE BANKING
⇗ Kimball — Null Foreign Keys & Default Rows ⇗ dbt — Handling NULLs in Models
✦ Eliminates silent data loss — every fact row is ✔ Always — every dimension table must have an
included in every aggregation Unknown member row; every fact FK must be NOT
NULL
✦ "Unknown" rows surface unresolved data as a
visible, quantifiable segment — not invisible gaps ✔ Source data has optional or late-populated FK
fields (optional customer on a transaction)
✦ NOT NULL FK constraint enforced at DB level
prevents bad data entering the DW ✔ Inferred member pattern is in use — the
placeholder and Unknown row serve different
✦ ETL quality is measurable — monitor how many
purposes (inferred = real entity, pending data;
rows route to the Unknown member over time
Unknown = genuinely unresolvable)
Description: The Date Dimension is the most important and universal conformed dimension
in any data warehouse. It has one row per calendar day and is pre-loaded for a range of years
(typically 10 years back, 5 forward). Unlike all other dimensions, it is never sourced from an
operational system — the DW team generates it entirely, adding rich calendar, fiscal, and
business attributes that no source system provides. Every fact table has at least one FK to
DimDate.
📅
Key Principle: Never join fact tables to a SQL date function at query time. Pre-
materialise every calendar attribute — fiscal year, week number, holiday flag, quarter
name — so BI tools and analysts can filter and group without writing complex date
arithmetic in every query.
Date key pattern: Use YYYYMMDD integer (e.g. 20240315) as the surrogate key —
not a DATE type. Integer keys are faster for joins, portable across databases, and
human-readable. Add a proper DATE column (full_date) separately for date arithmetic.
Reserve key value 0 = Unknown and 19000101 = Not Applicable.
Loaded via a Python/dbt seed script once; Without dual date keys, a quarter-end
fiscal calendar attributes set per org transaction in IST appears in the wrong
policy; never sourced from any fiscal quarter in UTC-based reporting
operational system
E-COMMERCE
✦ Eliminates complex SQL date functions in every ✔ Always — every data warehouse must have a
query — all calendar attributes are pre-computed Date Dimension, no exceptions
join columns
✔ Build it first, before any fact table — every fact
✦ Adds fiscal, business, and holiday context that table will need it
no source system provides natively
✔ Include fiscal calendar attributes early —
✦ Single pre-loaded table — zero ETL refresh retrofitting a fiscal calendar later is painful
needed after the initial load (new future dates
✔ Pre-load 10 years back and 5 years forward; add
appended annually)
annual extension as part of year-end DW operations
✦ Conformed across all fact tables — every mart
✔ For global DW: add local_date_key alongside
uses the same date definitions, quarter names,
utc_date_key in fact tables (role-playing dates)
fiscal periods
— Types 0 through 7
How a data warehouse responds to changes in dimension attribute values. Each type
represents a different tradeoff between historical accuracy, storage, and query complexity.
Description: The attribute value never changes. Once loaded, it is never updated regardless
of what changes in the source system. Used for truly immutable attributes that define the
original state of a record.
🔒
Use Case: Date of Birth, Original Credit Score, Account Open Date, SSN. These are facts
about an entity's origin that should never be overwritten — even if a source system
correction arrives.
Even if a data correction arrives saying DOB = 1985-04-13, a Type 0 field is never
updated. The original loaded value is the source of truth.
⇗ Immutable Attributes in DW
⇗ Healthcare Data Immutability
✦ Guarantees certain attributes are immutable — ✔ The attribute represents an immutable fact about
protects original onboarding data from accidental the entity's origin (date of birth, SSN, account open
overwrites date)
✦ No ETL logic required for these columns — they ✔ Business rules explicitly state "never update this
are loaded once and never touched again field regardless of source changes"
✦ Establishes a clear contract: these values ✔ Regulatory or audit requirements mandate that
represent the entity's original state forever the original value be preserved permanently
Description: The old value is simply replaced with the new value. No historical record is kept.
Simple but lossy — you can only ever see the current state. Used when history has no
analytical value, or for correcting data quality errors retroactively across all history.
✏️
Use Case: Typo Correction in Customer Name"Jon Smith" was a data entry error;
the correct name is "John Smith". Type 1 overwrites the name in place. No historical
analysis depends on the old misspelled name, so no history is needed.
C-001 Jon Smith New York C-001 John Smith New York
Phone number, email address, and contact SCD Type 1 is correct for data quality
preference fields are almost always Type 1 — corrections — the old value was never
contact info, not analytical attributes the truth, just an error
⇗ BigQuery — DML MERGE for SCD1 ⇗ Databricks — Delta Lake MERGE for SCD
✦ Simplest implementation — no surrogate key ✔ Historical value has zero analytical meaning —
versioning, no date-range logic only the current state matters
✦ No row growth — dimension table size stays ✔ Change is a correction of a data error, not a real-
constant regardless of update frequency world event (typo fix, phone format
standardisation)
✦ Retroactive correction propagates cleanly
across all historical fact rows automatically ✔ Business explicitly accepts that old facts will be
retroactively reassigned to new values
✦ Ideal for data quality fixes where the "old" value
was simply wrong ✔ Attribute changes infrequently and reporting
never compares old vs. new values
Description: When an attribute changes, the existing row is closed (end-dated) and a new
row is inserted with the new value, a new surrogate key, and new effective dates. The fact
table always links to the surrogate key, enabling exact point-in-time reconstruction. The most
widely used SCD type.
📊
Use Case: Employee PromotionAlice was promoted from "Analyst" to "Senior
Analyst" on 2024-03-01. Past sales facts should still reflect her as "Analyst" during that
period. A new row is added; old row is end-dated. Sales from Feb still join to the Analyst
row; sales from March join to the Senior Analyst row.
BANKING RETAIL
Without SCD Type 2: all 3 years of Loyalty tier is one of the most important
revenue would be attributed to the SCD Type 2 attributes in retail — drives
current RM — incorrect performance promotion eligibility and retrospective
measurement analysis
HR / CONSULTING
⇗ GCP — BigQuery SCD Type 2 Implementation ⇗ Databricks — Delta Lake SCD Type 2
✦ Complete, lossless history — every version of ✔ Historical analysis requires knowing what the
every attribute is permanently preserved attribute value was at the time of the fact — not
just today
✦ Point-in-time reconstruction is exact — join fact
to dim on date range to get the right version ✔ Attribute changes are meaningful business
events (promotions, address moves, price changes)
✦ Enables powerful "as-of" analysis: "what was
Alice's territory when she made that 2022 sale?" ✔ Change frequency is manageable — not millions
of updates per day (use RCD/mini-dim then)
✦ Industry default — all major DW platforms, dbt,
and BI tools are optimised for this pattern ✔ This is the default choice for most slowly
changing dimension attributes
✦ Surrogate key decouples the DW from source
system key changes ✔ Required by regulatory or compliance
frameworks that mandate point-in-time auditability
Description: Instead of adding a new row, a new column is added to store the previous
value. The row remains a single row but now has both current_value and previous_value
columns. Limited to tracking only one previous state — useful when you only ever need to
compare "before vs. after" a known one-time change.
🔄
Use Case: Sales Territory RealignmentA company reorganizes its sales territories
once. Analysts need to compare performance under old vs. new territories. Since this is
a one-time structural change, Type 3 (two columns) is sufficient — no need for full row
versioning.
Type 3 is ideal for planned, company- Use Type 3 only if this is a one-time
wide, one-time structural changes where event; if territory/region changes happen
only before/after comparison is needed regularly, use Type 2 instead
✦ No row growth — single row per entity even ✔ The change is a known one-time structural event
after the change (territory realignment, system migration,
rebranding)
✦ Current and previous values are immediately
visible side-by-side in any query ✔ Only one prior value ever needs to be tracked —
not a multi-version history
✦ No date-range joins required — both values
always accessible in the same row ✔ Analysts need to compare before/after the
specific change in a simple side-by-side report
✦ Minimal ETL complexity — just update two
columns when a change is detected ✔ The attribute is unlikely to change more than
once, or multiple changes are analytically irrelevant
Description: Current data lives in the main dimension table (always one row per entity,
always current). All historical versions are stored in a separate history table. The fact table
joins to the current dimension for today's view; historical analysis requires joining to the
history table.
📁
Use Case: Product PricingThe main DimProduct always shows the current price.
DimProduct_History has one row per price change event. Operational dashboards join to
DimProduct for speed; historical price analysis joins to the history table.
✓ Main dim stays lean and fast ✓ Full history separately preserved ✗ Two tables to
maintain ✗ History queries require extra join
✦ Main dimension stays compact and fast — ✔ Current-state queries dominate and need to be
operational dashboards never pay the cost of fast — historical queries are infrequent
historical rows
✔ An attribute changes frequently enough to bloat
✦ Complete history is still available in the history a Type 2 dimension but history must still be
table for compliance and audit queries retained
✦ Clear physical separation between current state ✔ Two separate access patterns exist: operational
and historical state simplifies BI tool configuration (current) and analytical (historical)
⚡
Use Case: Customer Credit ProfileIncome band, credit score tier, and risk rating
change monthly. They're split into a mini-dim. The fact table's mini-dim FK captures
which profile was active at transaction time. DimCustomer.current_profile_key (Type 1)
always reflects today's profile for operational lookups.
◈ TYPE 5 STRUCTURE
cust_key PK cust_key FK
name, city profile_key FK (historical)
current_profile_key ← Type1 overwrite
DimCustProfile (mini-dim)
profile_key PK
income_band
credit_tier
risk_rating
✦ Fast-changing attributes are isolated in a mini- ✔ A subset of dimension attributes changes far
dim, preventing row explosion in the base more frequently than the rest (monthly re-scoring)
dimension
✔ Both historical profile at transaction time AND
✦ Historical profile at transaction time is current profile for present-day reporting are needed
preserved via the fact table FK to the mini-dim
✔ Going through the fact table to find the current
✦ Current profile is always accessible directly from profile is too expensive for operational queries
the base dimension without touching the fact
✔ The fast-changing attributes have limited distinct
✦ Best of both worlds: performance (current view combinations (suitable for a mini-dim structure)
shortcut) + accuracy (historical point-in-time)
Description: Combines all three core types: Type 2 (new row per change with surrogate key
+ effective dates), Type 3 (current_value column on every row, overwritten Type 1 style), and
a historical_value column. Every row has the point-in-time value AND the current value. This
lets analysts write queries without date-range joins for current analysis while still supporting
full historical reconstruction.
🏆
Use Case: Sales Rep Territory — Current + Historical in One RowManagement
wants both: "What region was this rep in when this sale was made?" AND "What region
are they in now?" Type 6 puts both answers in the dimension row itself.
✓ Historical AND current in same row ✓ Most flexible for BI tools ✗ ETL must update
current_* on all prior rows ✗ More complex to implement
RETAIL HR / CONSULTING
⇗ Databricks — SCD Type 6 with Delta ⇗ Medium — SCD Type 6 Gold Standard
✦ Every historical row carries both its point-in- ✔ Analysts routinely ask both "what was X at the
time value AND the entity's current value — no time of the event?" AND "what is X today?" on the
second lookup needed same report
✦ BI tools can offer analysts a one-click toggle ✔ ETL team can handle the added complexity of
between "as-of" and "current" views updating current_* columns on all prior rows
✦ Eliminates complex date-range join logic for ✔ The dimension does not change at extremely
current-state reporting on historical fact sets high frequency (which would make bulk updates
expensive)
✦ The gold standard for analytics-heavy DW
where both historical accuracy and current ✔ Enterprise BI environment with power users who
context are critical need sophisticated temporal analysis
Description: The fact table carries TWO foreign keys for the same dimension: (1) a surrogate
key pointing to the historical Type 2 row, and (2) a durable natural key pointing to the current
row. The Type 2 dimension also has a current-view dim (Type 1 style). This achieves the same
result as Type 6 but through key relationships rather than physically overwriting current
columns.
🔑
Use Case: Dual-View Customer AnalysisAnalysts can toggle between "analyze by
customer's region at time of sale" (join via surrogate key) vs. "analyze by customer's
current region" (join via durable key) — both available from the same fact table row.
FACTSALES — DUAL FK
✦ Achieves Type 6 functionality without physically ✔ Type 6 is the target functionality but bulk UPDATE
overwriting current_* columns on all prior rows of prior Type 2 rows is too costly
✦ ETL update burden is eliminated — no mass ✔ The database engine or pipeline framework
update of historical rows required handles dual FK joins efficiently
✦ Dual FK pattern makes the dual-view intent ✔ The BI/analytics layer can abstract the dual FK
explicit and visible in the fact table schema complexity away from end users
✦ Works well in environments where UPDATE ✔ The team prefers a key-relationship solution over
operations on large dimension tables are a physical data overwrite approach
expensive
Description: SCD Type 2 is the most widely used change handling pattern, but the concept
card alone doesn't show how to implement the versioning metadata. Every SCD Type 2 table
must carry four standard engineering columns: effective_from_date , effective_to_date ,
is_current , and row_version . These enable point-in-time reconstruction without
recursive CTEs and allow BI tools to always retrieve the current row efficiently.
ETL process in 4 steps: (1) Hash-compare incoming source row against current DW
row. (2) If changed: UPDATE current row → set effective_to_date = yesterday, is_current
= 0. (3) INSERT new row → effective_from = today, effective_to = 9999-12-31,
is_current = 1, row_version = prior + 1. (4) If new entity: INSERT with effective_from =
business open date or load date.
E-COMMERCE TELECOM
BANKING
⇗ Databricks — Delta Lake MERGE SCD2 ⇗ Snowflake — Streams & SCD2 Tasks
✦ row_version provides an auditable sequence of ✔ Add row_hash to all Type 2 dims to simplify ETL
changes per entity change detection logic
✦ load_timestamp separates ETL processing time ✔ Include load_timestamp on every row to support
from business validity time pipeline auditing and late-data investigation
(RCD)
When attributes change so frequently that SCD Type 2 would cause unmanageable row
explosion. RCD patterns isolate the volatile attributes into separate structures to maintain
performance.
Description: Split the fast-changing attributes out of the main dimension into a separate,
smaller "mini-dimension." The fact table carries a FK to both. The main dimension retains only
stable attributes and uses SCD Type 1 or Type 2 as appropriate. The mini-dimension grows by
appending new rows as attribute values change.
📈
Use Case: Customer Risk ProfileA financial services company re-scores 2M
customers monthly on income_band, credit_tier, and risk_rating. If kept in DimCustomer
as Type 2, this would add 2M rows/month. Instead, these 3 attributes go into
DimCustProfile — a mini-dim with only a few thousand distinct combinations.
Alice's profile changed from P-10 (Mid/B/Low) to P-11 (High/A/Low) by April. The fact
table captures which profile was active at each transaction — without touching
DimCustomer at all.
CIBIL / Experian Customer Credit Score AIIMS / Manipal Hospital Patient Risk
45M customers have credit scores re- Stratification
calculated monthly. Applying SCD Type 2 to Patients are re-stratified into risk bands (Low/
score_band, credit_tier, and income_band Medium/High/Critical) after each consultation
would add 45M×3 = 135M new rows/month. based on BMI, BP, and diabetes status. 8M
Mini-dim solution: DimCreditProfile (18 patients × quarterly re-stratification = 32M
combinations of score_band × credit_tier × SCD2 rows/year if in base dim. Mini-dim:
income_band = 18 rows, static). Each fact row DimPatientRisk (12 rows: 3 risk_levels × 2
carries credit_profile_key capturing the exact diabetes_status × 2 hypertension_status).
profile at transaction time. FactTreatmentClaim carries patient_key and
risk_profile_key independently.
DimCreditProfile: 18 rows, never grows.
DimCustomer: 45M rows, stable. Mini-dim is essential when re-
FactTransaction: credit_profile_key classification happens at high frequency
changes monthly per customer. across a large population — classic RCD
use case
✦ Eliminates the row explosion that SCD Type 2 ✔ A dimension has attributes that change very
would cause for high-frequency attribute changes frequently (monthly, weekly) for a large population
✦ Main dimension stays compact — only stable ✔ Applying SCD Type 2 to these attributes would
attributes, loaded once or rarely cause unacceptable row growth (e.g. 2M rows/
month)
✦ Point-in-time profile preserved in the fact table
FK — no analytical accuracy is lost ✔ The fast-changing attributes form meaningful
analytical groups (income band, risk tier)
✦ Mini-dim has far fewer rows than the base dim
— combinations, not per-entity versions ✔ Both historical accuracy (at transaction time) and
current state are required for analysis
Description: Some attributes that appear to be dimension attributes are actually numeric
measures masquerading as dimensions. The solution is to move them out of the dimension
entirely and into a periodic snapshot fact table, where they belong as measures.
💹
Use Case: Product PriceProduct price changes daily. It feels like a product attribute
but it's actually a measure. Instead of DimProduct.current_price (which would require
daily Type 2 rows), create FactDailyProductPrice with one row per product per day.
FactDailyProductPrice: 80,000 SKUs × 365 Exchange rates are the canonical example
days = 29M rows/year — manageable and of a numeric measure that superficially
analytically correct looks like a dimension attribute
✦ Correctly reclassifies a numeric measure that ✔ An attribute is actually a numeric measure (price,
was wrongly modelled as a dimension attribute score, rate) masquerading as a dimension column
✦ DimProduct remains stable and compact — no ✔ The value changes continuously and every data
pricing churn pollutes it point at each time period has independent
analytical value
✦ Full price history is naturally available via
periodic snapshot fact table query ✔ The cadence of change maps naturally to a
periodic snapshot (daily price, monthly rate)
✦ Aligns with first-principles dimensional
modelling: measures belong in fact tables ✔ Examples: product pricing, exchange rates,
interest rates, benchmark scores
Description: When the rapidly changing attributes are low-cardinality flags or indicators (not
continuous values), pre-build all possible combinations into a junk dimension. Since every
possible combination already exists in the table, new "changes" simply point to a different
existing row in the junk dim — the junk dim itself never grows.
🏷️
Use Case: Subscription Status FlagsA SaaS platform customer's subscription_tier
(Free/Pro/Enterprise), is_trial (Y/N), and auto_renew (Y/N) change frequently. With
3×2×2=12 combinations, the junk dim has 12 static rows. Customer "changes" just
update which row they point to in the fact or a current-profile table.
1 Free N N
2 Free Y N
5 Pro N Y
9 Enterprise N Y
✓ Junk dim is completely static ✓ Changes cost-free (just update FK) ✗ Only works
for low-cardinality combos
E-COMMERCE BANKING
✦ Junk dim is static — never needs inserts or ✔ The fast-changing attributes are categorical flags
updates as flags change for individual entities or indicators with very low cardinality each
✦ "Changes" are just FK updates pointing to a ✔ Total combination count across all flags is small
different pre-existing combination row and manageable (under ~1,000 rows)
✦ All flag combinations are filterable in a single ✔ The attribute set is stable — new flag types are
dimension join added infrequently
✦ No row explosion whatsoever — bounded by ✔ The junk dim pattern already exists in the model
total possible combinations — extending it is the natural fit
Modeling
Two independent timelines per record. The most comprehensive approach to temporal data
management — supports both "what was true in reality" and "when did we know it"
independently.
Description: Every record carries two independent timelines: Valid Time (when the fact was
true in the real world) and Transaction Time (when the record was stored in the database).
This allows answering four types of questions that no single-timeline model can: current
reality, historical reality, what we knew now about the past, and what we knew then about the
past.
Alice moved from Boston to Chicago on Feb 1, but didn't update her profile until Feb 15. Then
on Mar 10, she corrects the record with a backdated Boston address (she was actually in
Boston until Jan 31, not Feb 1).
CUST_ID CITY VT_FROM (VALID) VT_TO (VALID) TT_FROM (TRANSACTION) TT_TO (TRANSACTION)
📍 ⏮️ 🕵️
"Current "Jan 15 "What we knew on
Reality"WHERE vt_to Reality"WHERE Feb 10"WHERE
= '9999' AND tt_to = '2024-01-15' BETWEEN '2024-02-10' BETWEEN
'9999' → Chicago vt_from AND vt_to AND tt_from AND tt_to → we
tt_to = '9999' → Boston thought: Boston until
Feb 1
Use only when needed: Bi-temporal is the most complex and storage-intensive
pattern. Use it in regulated industries (banking, healthcare, insurance) where audit
trails must prove "what did we believe, and when did we believe it."
HEALTHCARE
✦ Answers all four temporal questions: current ✔ Regulated industry requires proving what the
reality, historical reality, what we knew now, what system believed at a specific recorded time
we knew then (banking, insurance, healthcare)
✦ Supports retroactive corrections without ✔ Source data corrections arrive after the fact and
destroying the prior recorded state — both must be recorded without overwriting the original
versions coexist recording
✦ Full audit trail: prove to regulators exactly what ✔ Auditors or compliance teams need to
the system believed at any point in time reconstruct both the real-world state and the
system's knowledge state independently
✦ Handles late-arriving corrections gracefully — a
new TT row is added without altering old TT rows ✔ The analytical team is mature enough to work
with two-timeline query patterns
Schema
CHAPTER 06
Patterns
How fact and dimension tables are physically arranged and connected. Schema choice
impacts query performance, storage efficiency, and ETL complexity.
Description: One central fact table surrounded by denormalized dimension tables. Each
dimension is a single table with all attributes fully denormalized (e.g., DimStore contains city,
state, country — not normalized into sub-tables). Named "star" because the ER diagram looks
like a star. The dominant pattern for data warehouses and BI workloads.
⭐
Use Case: Retail Analytics DWA retailer builds a star schema with FactSales at
center, surrounded by DimDate, DimProduct, DimStore, DimCustomer, DimPromotion.
All joins are single-hop — fact to dim. BI tools like Tableau, Power BI, Looker perform
best on star schemas.
DimDate
date_key PK
full_date, month
quarter, year
FactSales
date_key FK
DimProduct product_key FK
DimStore
DimCustomer
customer_key PK
name, segment
city, country
✓ Simple, fast queries (1 join per dim) ✓ BI tool friendly ✓ Easy for analysts to
understand ✗ Some data redundancy in dims
RETAIL HEALTHCARE
Reliance Retail / DMart Sales Data Apollo Hospitals Patient Billing Mart
Mart FactPatientBilling → DimPatient, DimDoctor,
FactSales (centre) with DimDate, DimProduct, DimProcedure, DimDate, DimInsurancePlan,
DimStore, DimCustomer, DimPromotion as DimWard. Hospital management uses Power
direct dimension spokes. Tableau/Power BI BI on this star schema. No joins beyond one
connects directly — all joins are single-hop. A hop — billing analysts without SQL expertise
store manager's "daily sales by category" can self-serve using drag-and-drop in the BI
report executes in <2 seconds on BigQuery tool.
because there are no snowflake join chains.
Healthcare billing mart: 6 dimensions,
Star schema is the universal default for all fully denormalised into star schema
BI-facing layers; all 6 major BI tools for non-technical analyst self-service
have native star schema optimisation
⇗ BigQuery — Star Schema Best Practices ⇗ dbt — Mart Structure (Star Schema)
✦ Single-hop joins from fact to every dimension — ✔ Building a presentation/reporting layer for BI
maximum query performance tools and business analysts
✦ All major BI tools (Tableau, Power BI, Looker, ✔ Query performance and simplicity are higher
Metabase) are natively optimised for star schema priorities than perfect storage normalisation
✦ Intuitive structure — business analysts can ✔ Dimensions are stable and not subject to
understand and self-serve without SQL expertise frequent hierarchical restructuring
✦ Denormalized dims reduce join complexity in ✔ This is the recommended default for all DW
queries and simplify ETL logic presentation layers — use it unless there is a
specific reason not to
✦ Industry default for DW presentation layers —
most tooling, best practices, and talent assume
star schema
Description: Dimension tables are normalized — their attributes are broken out into sub-
dimension tables. DimProduct might reference DimCategory, DimBrand. This reduces storage
redundancy but introduces multi-hop joins. Named "snowflake" because the branching ER
diagram resembles a snowflake.
❄️
Use Case: Product HierarchyProduct belongs to a subcategory, which belongs to a
category, which belongs to a department. In a snowflake, these are separate tables.
This is storage-efficient if the hierarchy changes frequently, but adds join complexity.
DimProduct
FactSales product_key PK
product_key FK
→ product_name
→
subcat_key FK →
DimSubCategory DimCategory
subcat_key PK cat_key PK
subcat_name
→ category_name
cat_key FK → dept_key FK →
MANUFACTURING RETAIL
✦ Reduces storage footprint for very large ✔ Dimension hierarchies are very deep and the
dimension tables with deep hierarchies hierarchy structure changes frequently
✦ Hierarchy changes (e.g. category restructuring) ✔ Storage is a hard constraint and dimension
update only one sub-dimension table redundancy is measurably significant
✦ Enforces referential integrity at the database ✔ The BI tooling in use handles multi-hop joins well
level through FK constraints (e.g. direct SQL access rather than semantic model)
Description: Multiple fact tables sharing conformed dimension tables. Each business process
gets its own fact table, and shared dimensions (DimDate, DimCustomer, DimProduct) are
reused across all of them. This is how enterprise data warehouses are built — multiple stars
connected through shared conformed dims.
🌌
Use Case: Retail Enterprise DWFactSales, FactInventory, FactReturns all share
DimDate, DimProduct, and DimStore. Analysts can "drill across" — compare sales
revenue vs. inventory levels vs. return rates on the same product and date axis.
FactSales FactInventory
date_key FK date_key FK
product_key FK product_key FK
FactReturns
date_key FK
product_key FK
DimDate (CONFORMED)
date_key PK
DimProduct (CONFORMED)
product_key PK
✦ Scales naturally as new business processes are ✔ Multiple teams build separate data marts that
added — just add another fact table sharing need to interoperate through shared dimension
existing conformed dims definitions
Description: A methodology for modeling the raw ingestion layer of a data warehouse. Uses
three table types: Hubs (business keys), Links (relationships between business keys), and
Satellites (descriptive attributes + history). Hash keys replace integer sequences. Highly
parallelizable, source-agnostic, and fully auditable. Typically sits beneath the Kimball
presentation layer.
🏗️
Use Case: Enterprise Data Integration LayerIntegrating Customer data from 5
source systems (CRM, billing, support, marketing, app). Each source feeds into the same
Hub_Customer (keyed by business key). Satellites per source capture source-specific
attributes. Links capture relationships (customer-to-order, customer-to-account).
Hub_Customer
business_key, hash_key, load_date, rec_src
Link_CustomerOrder Hub_Order
cust_hash_key + order_hash_key order_id, hash_key
Bajaj Allianz / ICICI Lombard Multi- IndusInd / RBL Bank RBI CRILC
Source Integration Reporting
Claims data comes from 6 legacy systems: RBI's Central Repository of Information on
LegacyMotor, LegacyHealth, LegacyLife, Large Credits requires tracking every loan
AgentPortal, BancassurancePartner, relationship across multiple bank entities
ReinsuranceSystem. Each has different natural with full audit history. Data Vault:
keys for "customer". Data Vault: Hubs store Hub_Borrower, Hub_LoanFacility,
each system's natural key; Links resolve cross- Link_BorrowerFacility, Sat_BorrowerDetails
system customer relationships; Satellites store (versioned), Sat_FacilityTerms (versioned).
version-controlled attributes per source. New regulatory reporting requirements add
Presentation layer builds star schemas from new Satellites without touching existing
vault for BI. ones.
6 source systems with conflicting customer Data Vault additions for new regulatory
IDs — Data Vault's Hub-Link-Satellite requirements never require existing Sat
separation is the only clean integration tables to be altered — critical for live
architecture regulatory systems
⇗ Data Vault 2.0 — Dan Linstedt ⇗ Data Vault 2.0 — Dan Linstedt
⇗ Databricks — Data Vault 2.0 ⇗ dbt — Data Vault Packages & Patterns
✦ Fully parallelisable loads — Hubs, Links, and ✔ Integrating data from many heterogeneous
Satellites load independently with no inter- source systems with different natural keys and
dependency contention schemas
✦ Every record has full audit metadata: source ✔ Full audit trail and data lineage are hard
system, load timestamp, hash key — traceability regulatory requirements (financial services,
is built-in government)
✦ Schema is source-agnostic — adding a new ✔ Load parallelism is critical — very high data
source system adds new Satellites, never alters volumes that sequential loading cannot handle
existing ones
✔ The team has tooling (dbt vault packages,
✦ Historical changes are preserved automatically WhereScape) to automate the boilerplate
via Satellite versioning — no SCD decisions
✔ A Kimball star schema presentation layer will sit
needed per attribute
above it for analyst consumption
Description: All facts and dimension attributes are pre-joined and denormalized into a single
wide table. Leverages columnar storage engines (BigQuery, Snowflake, Redshift) which
compress repeated values efficiently. Eliminates join overhead entirely at query time.
Common in the modern "semantic layer" and reverse-ETL era.
📊
Use Case: BigQuery Analytics LayerA startup with 10M rows pre-joins all dimension
attributes into one wide table. No joins at query time. BigQuery columnar compression
makes repeated strings (like "United States" in country column) storage-efficient. Tools
like Metabase, Preset, or Hex perform very fast on OBT.
✓ Zero join overhead ✓ Ideal for columnar engines ✓ BI tools love it ✗ Harder to
manage dimension changes ✗ Some redundancy
OBT is the pragmatic choice for startups: BigQuery's columnar storage compresses
ship in 1 week vs star schema in 6 weeks; repeated strings like state names to
technical debt addressed at Series B near-zero overhead — OBT storage penalty
scale is <5% vs normalised star schema
⇗ BigQuery — Nested & Repeated Fields (OBT) ⇗ Medium — OBT in Modern Analytics
✦ Zero join cost at query time — all attributes ✔ The analytics engine is a columnar cloud DW
already co-located in one table (BigQuery, Snowflake, Redshift, DuckDB)
LAYER ARCHITECTURE
History All versions Deduped — 1 record per SCD Type 2 for dim
retained entity state versioning
Grain Source event Logical entity grain Declared fact grain per
grain subject area
⇗ FHIR on Azure
✦ Separation of concerns — each layer has one ✔ Building on a cloud lakehouse platform
job; quality issues at one layer don't cascade (Databricks, BigQuery, Snowflake, Azure Synapse)
forward
✔ Multiple teams consume data at different quality
✦ Full data lineage preserved — Bronze is levels (engineers need raw; analysts need clean)
immutable so you can always replay from raw
✔ Data lineage, replay, and auditability are
✦ Incremental transformations are simpler — each requirements — Bronze immutability provides this
layer only needs to process changes from the
✔ dbt is the transformation tool — Medallion maps
layer below
directly to staging / intermediate / mart model
✦ Multiple consumers can access the appropriate layers
layer for their use case without competing on one
✔ Default recommendation for all new cloud DW /
table
lakehouse builds — prefer this over ad-hoc schema
✦ Aligns naturally with dbt project structure organisation
(staging → intermediate → marts)
Hierarchy
CHAPTER 07
Patterns
Description: A hierarchy with a known, fixed number of levels. Each level is stored as a
separate column in a single denormalized dimension table. Simple, fast, and the default
approach for most hierarchies in a star schema.
📅
Use Case: DimDate — Year/Quarter/Month/DayEvery date dimension has exactly 4
levels. Store all four as columns in one row. Rollups are trivial GROUP BY queries.
◈ FIXED-DEPTH TREE
Year: 2024
Quarter: Q1
Month: March
Day: 2024-03-01
Day: 2024-03-02
Day: …
✦ All hierarchy levels available as direct columns ✔ Hierarchy has a known, stable number of levels
— no recursive queries or path traversal needed that never varies across members
✦ GROUP BY at any level is trivial — year, quarter, ✔ All members fill all levels consistently — no
month, day are all simple column references skipping or ragged depth
✦ BI tools handle fixed-depth hierarchies natively ✔ This is the default choice for Date, Geography
— drill-down just works out of the box (Country→Region→City), and Product hierarchies
✦ Simplest possible storage: one row per leaf- ✔ Use this pattern unless the hierarchy is genuinely
level member with all ancestor levels variable-depth or self-referencing
denormalised
Description: A hierarchy where not all branches have the same depth. Some members skip
levels. For example, a geographic hierarchy where "United States → California → San
Francisco" has 3 levels, but "Vatican City" (a country with no regions or cities) has just 1 level.
NULL padding is the common solution.
🌍
Use Case: Global GeographyLarge countries have Country→Region→City. Small
countries may have Country→City (no region). Some territories skip directly from
Country to Outlet. NULL padding fills missing levels.
✦ Accurately models real-world geographies and ✔ The real-world hierarchy genuinely has variable
org structures that don't fit neat uniform levels depth across members (global geography, complex
org charts)
✦ NULL padding keeps the schema consistent —
same column structure for all members ✔ The maximum depth is still known and bounded
— NULL padding is feasible
✦ COALESCE tricks allow rollup queries to still
work correctly across varying depths ✔ Analytical teams understand how to handle
NULLs in rollup queries
Description: The table references itself — each row has a parent_key pointing to another row
in the same table. Maximum flexibility for arbitrary-depth trees, but requires recursive CTEs or
materialized path structures for efficient querying. Used for org charts, bill of materials,
category trees.
🏢
Use Case: Organizational HierarchyAn org chart where every employee has a
manager. The CEO has no manager (NULL parent). The depth can vary across branches.
HR / CONSULTING MANUFACTURING
TCS / Cognizant Org Chart (100,000 Tata Steel / JSW Steel Bill of Materials
employees) A finished product (e.g. automotive steel coil)
DimEmployee has emp_key and manager_key has a BOM 8 levels deep: Finished Product →
(FK to same DimEmployee table). The org Sub-Assembly → Component → Sub-
chart is 10+ levels deep and restructures Component → Raw Material → Chemical
quarterly. WITH RECURSIVE CTE traverses the Composition. parent_part_key in DimPart
tree: "Find all employees under BU Head X" enables recursive BOM explosion: "total raw
walks the parent_key chain to unlimited depth. material cost for product X" traverses the full
Employee count by subtree, budget rollups, tree recursively to leaf nodes.
and reporting-line analysis all use recursive
CTE. BOM explosion via recursive CTE is the
canonical manufacturing use case —
Recursive CTEs on 100K-node trees hierarchy depth varies per product so
complete in <3 seconds in BigQuery/ recursive dim is the only option
Snowflake; pre-materialise closure table
for sub-second performance
⇗ Kimball Recursive Parent-Child
⇗ Snowflake — Recursive CTEs for Hierarchies ⇗ SQL Server — Hierarchical Data & CTEs
✦ Compact storage — just two columns (member ✔ The hierarchy structure changes frequently —
key + parent key) per row regardless of tree parent reassignments, new levels added
depth
✔ The query engine supports recursive CTEs well
✦ Adding new hierarchy members is trivial — just (Snowflake, BigQuery, PostgreSQL, SQL Server)
insert a row with the correct parent key
✔ If query performance is critical, combine with a
✦ Naturally models real-world org charts, BOM closure table for fast tree traversal
structures, and folder hierarchies
✔ Examples: employee org chart, bill of materials,
product category tree, folder structures
Description: A materialized table that stores every ancestor-descendant pair for all nodes in
the hierarchy, along with the depth of the relationship. Pre-computes all paths so tree
traversal requires no recursion at query time — just a simple join.
🌳
Use Case: Product Category RollupPre-materialize all ancestor-descendant pairs so
you can answer "Give me sales for all products under the Electronics department" with
a simple JOIN — no recursive CTE needed.
1 Electronics NULL 1 1 0
2 Phones 1 1 2 1
3 Smartphones 2 1 3 2
2 2 0
2 3 1
3 3 0
Amazon / Flipkart Product Category Wipro / HCL Org Chart Subtotals for
Taxonomy Dashboards
Electronics → Computers → Laptops → Gaming Executive dashboard shows "total headcount
Laptops is 4 levels, but Electronics → and revenue under each VP" — recalculated
Accessories → Cables is 3 levels. Closure nightly for 500 VPs. Recursive CTE takes 45
table: one row per ancestor-descendant pair seconds. Closure table (pre-materialised
per level. "All products under Electronics" = nightly): "headcount under VP X" = simple
JOIN BridgeCategoryPath WHERE ancestor_key SUM with closure join, 0.3 seconds. Dashboard
= ELECTRONICS_KEY — no recursion, no CTE. becomes real-time interactive instead of 45-
Executes as a simple hash join in BigQuery. second batch refresh.
✦ All ancestor-descendant relationships are pre- ✔ Tree traversal queries are frequent and
materialised — tree traversal is a simple JOIN, no performance is critical (product category rollup, org
recursion chart subtotals)
✦ Subtotal queries ("all sales under Electronics") ✔ The SQL engine does not support recursive CTEs,
are extremely fast even on large trees or recursive query performance is unacceptable
✦ Depth column enables level-specific filtering ✔ The hierarchy changes infrequently enough that
("show only direct children" vs "all descendants") pre-materialising the closure table is manageable
✦ Works on any SQL engine — no recursive CTE ✔ Combine with the parent-child recursive dim —
support required closure table is the performance layer on top of it
Concepts
Grain is the single most important design decision in dimensional modeling. "What does one
row in this fact table represent?" must be answered precisely before any other design
decisions are made.
Description: The lowest possible level of detail that an operational process produces. One
row = one scan at checkout, one click, one payment line. Atomic grain tables can answer ANY
question at any level of rollup. The Kimball methodology strongly recommends always
building the atomic grain first.
⚛️
Example grain declaration: "One row represents one line item on one sales
transaction at one store on one day for one customer." — This is atomic. Any coarser
grain (one row per day per store) loses the ability to drill into individual transactions.
Kimball's Rule: Declare the grain first. Then identify all dimensions that are
meaningful at that grain. Then identify all facts that exist at that grain. Any fact that
doesn't exist at atomic grain belongs in a different fact table.
Aggregate Grain: Pre-summarized fact tables at a higher level of granularity — one row per
product per month instead of per transaction. Used for performance optimization when
common queries always roll up to a certain level. Aggregate tables should be in addition to,
not instead of, the atomic fact table.
Grain Conflict: The most common and dangerous mistake in dimensional modeling — mixing
rows of different granularity in the same fact table. This causes double-counting and incorrect
aggregations.
Grain Conflict Danger: SUM(revenue) on this table would double-count P-042 sales
at ST-07 on 2024-03-01. The daily total (152.40) and individual transactions already
sum to more than actual sales. Always put different grains in separate fact tables.
RETAIL HEALTHCARE
RETAIL FINANCE
⇗ dbt — Materialised Models & Aggregates ⇗ Snowflake — Dynamic Tables for Aggregation
✦ Transparent to BI tools — aggregate navigation ✔ Never mix grains in a single fact table — if rows
can be handled automatically by the semantic represent different things, separate them into
layer different tables
⚡
Use Case: Monthly Sales ReportFactSales (atomic) has 500M rows at transaction
grain. A monthly summary report scans all 500M rows every time to GROUP BY month.
With aggregate awareness: FactSalesMonthlySummary (1.2M rows at
month×product×store grain) is used instead. Query time: 45s → 0.3s.
Maintenance discipline: Aggregate tables must be kept consistent with the atomic
base. If the atomic fact is updated or backdated, all affected aggregate tables must be
reprocessed. Document dependencies explicitly in your data lineage tooling (Dataplex,
dbt docs, Collibra).
Tata Retail / Shoppers Stop on Looker Axis Bank / Yes Bank Regulatory
Three aggregate tables registered in the Reporting Aggregates
Looker semantic layer: FactSalesDaily (18M Daily regulatory reports (LCR, NSFR, CRR)
rows), FactSalesMonthly (1.2M rows), require specific aggregate grains:
FactSalesAnnual (50K rows). Looker's FactDailyLiquidityPosition (product × currency
aggregate awareness routes "YTD revenue by × maturity_band grain). Pre-materialised as a
region" to FactSalesAnnual automatically. BigQuery scheduled query from atomic
"This week's daily trend" goes to FactTransaction. RBI report generation: 0.8
FactSalesDaily. The analyst writes one Looker seconds from the aggregate vs 38 seconds
Explore query — routing is transparent. from atomic fact. Materialised view
invalidation triggers reprocessing when atomic
Looker aggregate awareness configuration: fact changes.
10 lines of YAML per aggregate table in
the LookML model; zero SQL changes BigQuery materialised views with
incremental refresh provide aggregate
awareness transparently for SQL-based
⇗ Kimball Aggregate Navigation
reporting tools
✦ Dramatic query performance improvement for ✔ Specific high-frequency report queries are
common rollup reports — orders of magnitude measurably slow on the atomic fact table
faster
✔ The rollup grain (daily, monthly, yearly) is stable
✦ Reduces cloud compute cost — smaller scans and well-understood — not ad-hoc
mean less bytes processed = lower bill on
✔ The BI or semantic layer supports aggregate
BigQuery/Snowflake
navigation (dbt semantic layer, Looker, Power BI
✦ Transparent to end users — they write one aggregations)
query; the semantic layer routes to the best table
✔ Always build on top of the atomic fact table —
✦ Atomic fact table remains the single source of never replace it with the aggregate
truth — aggregates are always derivable
✔ Monitor aggregate usage and drop any aggregate
that is never used (maintenance cost not justified)
Key
CHAPTER 09
Strategies
The technical plumbing of dimensional models. Key strategy choices affect SCD correctness,
join performance, pipeline complexity, and data quality.
EMP_KEY (SK — DW GENERATED) EMP_NATURAL_KEY (NK — FROM HR SYSTEM) NAME TITLE IS_CURRENT
E-COMMERCE HR
✦ Enables SCD Type 2 — each version of a ✔ Always use surrogate keys as the PK for all
dimension row gets its own unique SK dimension tables — this is the Kimball standard
✦ Decouples the DW from source system key ✔ SCD Type 2 is in use — surrogate keys are
changes — if the source renames a key, the DW is mandatory to version dimension rows
unaffected
✔ Source system natural keys are unstable, reused,
✦ Integer PKs provide faster joins than string- or composite (multi-column)
based natural keys
✔ Retain natural keys as a separate column for
✦ Protects against null, duplicate, or reused joining back to source systems and for human
natural keys from source systems readability
Description: A DW-assigned surrogate key that is stable across all Type 2 versions of a
dimension row. Unlike the regular surrogate key (which changes per version), the durable key
stays constant across all rows for the same real-world entity. Used in SCD Type 7 as the
"current view" FK in the fact table.
BANKING HR / CONSULTING
Durable key survives corporate mergers, Durable key is critical for SCD Type 6
system migrations, and natural key and Type 7 architectures — without it,
reassignments — assigned once, never re-hires or re-registered customers look
changed like new entities
⇗ Kimball — Durable Supernatural Key ⇗ Medium — Durable Key in SCD Type 6 & 7
✦ Single stable key that identifies an entity across ✔ SCD Type 6 or Type 7 is in use and current-view
all its Type 2 historical versions access from historical fact rows is required
✦ Enables "current view" lookups without ✔ The natural key is unstable (can change over
scanning all historical rows or filtering on time) but entity continuity must still be tracked
is_current
✔ Analysts need to group all historical versions of
✦ Used in SCD Type 7 as the dual FK — fact table an entity under a single stable identifier
can access both historical and current views
✔ Always assign the durable key at the entity's first
simultaneously
appearance and never change it
✦ Simplifies "who is this entity today?" queries
without date-range logic
Description: A cryptographic hash of the business key (or a combination of columns for
change detection). Used in two contexts: (1) Data Vault — hash keys replace integer
sequences, enabling parallel loads without sequence contention. (2) Delta detection — a hash
of all tracked columns creates a single fingerprint; if it changes, the row has changed.
🔐
Use Case 1 — Data Vault Hub Key: SHA256(customer_id) → deterministic,
reproducible, parallelizable. Multiple pipelines can compute the same hash
independently without a central sequence generator.
🔍
Use Case 2 — SCD Delta Detection: Hash all tracked columns per row. Compare
incoming hash to stored hash. If different → change detected → SCD update triggered.
Used heavily in dbt and BigQuery MERGE patterns.
GCP-Based Delta Ingestion Framework Bajaj Allianz / New India Data Vault 2.0
In a BigQuery delta ingestion pipeline, each In Data Vault, Hub_Policy has policy_hk =
row arriving from source has a row_hash = SHA-256(source_system || "||" ||
SHA-256(col1 || col2 || … || colN) computed in policy_natural_key). Multiple source systems
Dataflow. MERGE INTO target: WHEN can compute the same hash independently —
MATCHED AND row_hash != target.row_hash no coordination, no sequence generator, no
THEN UPDATE, WHEN NOT MATCHED THEN contention. 12 parallel Dataflow pipelines all
INSERT. 200M row comparison in 4 minutes compute and insert to Hub_Policy
using only hash column comparison — no 40- simultaneously; identical hashes naturally
column CASE WHEN logic. deduplicate via MERGE.
Hash-based change detection eliminates Data Vault hash key is the enabler of
column-by-column comparison: 1 string fully parallelised DW loads — it
comparison per row vs N column eliminates the global sequence generator
comparisons; 3× faster MERGE on BigQuery bottleneck that serialises pipeline
execution
⇗ Databricks — Data Vault Hash Keys ⇗ dbt Utils — Hash Surrogate Key Macro
⇗ BigQuery — SHA256 & MD5 Functions ⇗ Medium — Hash Keys in Data Vault 2.0
✦ Deterministic and reproducible — the same ✔ Data Vault 2.0 architecture — hash keys are
natural key always produces the same hash, mandatory for Hub, Link, and Satellite design
enabling parallel loads
✔ High-volume parallel ingestion pipelines where
✦ Data Vault: eliminates sequence generator sequential surrogate key generation is a bottleneck
contention — multiple pipelines compute identical
✔ SCD change detection in dbt or BigQuery MERGE
hashes independently
patterns — hash all tracked columns into one
✦ Delta detection: hashing all tracked columns row_hash
into a single fingerprint makes change detection
✔ Use SHA-256 for collision resistance in large
a simple string comparison
datasets; MD5 is acceptable for smaller datasets
✦ Compact change indicator — one hash column with lower risk
replaces N individual column comparisons in
MERGE logic
Description: The Kimball Bus Architecture is the enterprise-level planning framework that
defines how multiple data marts are designed to interoperate. Its central tool is the
Enterprise DW Bus Matrix — a planning document where rows are business processes
(subject areas) and columns are candidate dimensions. A cell is marked if that dimension is
used by that business process. Shared (conformed) dimensions appear in multiple rows,
forming the "bus" that connects all marts together.
🏗️
Why it matters: The Bus Matrix is built before any physical tables are created. It
reveals which dimensions must be conformed across marts, which subject areas share
grain, and what the incremental build order should be. It is the single most important
architectural planning artefact in Kimball methodology.
Retail Sales ✦ ✦ ✦ ✦ ✦
Inventory ✦ ✦ ✦ ✦
Procurement ✦ ✦ ✦
HR / Payroll ✦ ✦
Marketing Campaigns ✦ ✦ ✦ ✦
Reading the matrix: Date, Product, and Store appear across 4+ business processes
→ these must be conformed dimensions, built first, owned by the enterprise DW team.
Customer is shared by Sales and Marketing → must also be conformed. Vendor is only
in Inventory and Procurement → can be scoped to those marts initially.
Build order: Start with the subject area that has the most stakeholder value AND the
most shared dimensions. Typically: Retail Sales first (most conformed dims), then reuse
those conformed dims for Inventory. Each new mart should reuse existing conformed
dims wherever possible.
Bus Matrix review meeting: 3 hours with Financial services bus matrix: customer
business owners identified 3 candidate entity disambiguation is the hardest
"conformed" dims that were actually conformance problem — individual vs
different entities — saved 6 months of corporate vs SME are fundamentally
integration rework different entities
✦ Forces dimension conformance decisions to be ✔ Always — build the bus matrix before designing
made explicitly before any physical development any physical tables in an enterprise DW
begins
✔ Multiple business processes or subject areas are
✦ Communicates the DW architecture to business in scope (not a single-subject analytics product)
stakeholders without technical jargon
✔ Multiple teams will own different subject areas —
✦ Reveals the incremental build sequence — the matrix aligns them on shared dimensions
which mart to build first based on shared dim
✔ Present the bus matrix to business stakeholders
reuse
for alignment before starting any engineering work
✦ Creates a living reference document — the bus
✔ Maintain it as a living document — add rows
matrix evolves as new subject areas are added
when new business processes are added to the DW
✦ Prevents siloed mart development that
produces incompatible, non-interoperable data
products
Description: Drill-across queries combine metrics from two or more separate fact tables on
a shared conformed dimension axis — e.g. compare sales revenue vs. inventory levels vs.
marketing spend by product and month. Drill-through navigates from a summary aggregate
down to the underlying atomic transaction rows. Both patterns only work correctly when
dimensions are conformed. They represent the primary analytical payoff of the Kimball
architecture.
WITH sales AS (
SELECT d.year_month, [Link], SUM([Link]) AS sales_revenue
FROM FactSales f
JOIN DimDate d ON f.date_key = d.date_key ← conformed dim
JOIN DimProduct p ON f.product_key = p.product_key ← conformed dim
GROUP BY 1,2
),
marketing AS (
SELECT d.year_month, [Link], SUM([Link]) AS mkt_spend
FROM FactMarketingSpend m
JOIN DimDate d ON m.date_key = d.date_key ← SAME conformed dim
JOIN DimProduct p ON m.product_key = p.product_key ← SAME conformed
dim
GROUP BY 1,2
)
SELECT s.year_month, [Link], s.sales_revenue, m.mkt_spend,
ROUND(mkt_spend / NULLIF(sales_revenue,0) * 100, 1) AS
mkt_pct_of_sales
FROM sales s
LEFT JOIN marketing m USING (year_month, category);
BI tool implementation: Modern BI tools (Tableau, Power BI, Looker) support drill-
through natively — a user clicks a bar on a monthly chart and sees the underlying
transaction rows. This requires a defined "detail" data source pointing to the atomic
fact table, linked to the same conformed dimensions as the summary.
✦ Drill-across unlocks enterprise-level analytics — ✔ Multiple fact tables exist that share conformed
compare KPIs across completely separate dimensions — drill-across becomes immediately
business processes available
✦ Drill-through provides transparency and audit ✔ Business stakeholders ask cross-mart questions:
capability — no more "trust the dashboard" "how does our marketing spend compare to
without validation revenue by category?"
✦ Both patterns are the direct return on ✔ Executives need summary views but auditors
investment from building conformed dimensions need transaction-level traceability
correctly
✔ Pre-requisite: all dimensions involved in the
✦ Reduces the need for ad-hoc data pulls — cross-mart join must be fully conformed
analysts can self-serve from summary to detail
without engineering
Description: Every production DW table — both fact and dimension — should carry a
standard set of audit and technical columns. These columns are not business attributes and
are never exposed to end users in BI tools, but they are essential for pipeline monitoring, data
quality investigation, reprocessing, lineage tracing, and change detection. Defining them as a
standard across the entire DW prevents inconsistency and ad-hoc firefighting.
Separation rule: Audit columns must never be exposed in BI tool data sources or
semantic layer definitions. They are for engineers and data stewards only. Create a
separate "technical view" of each table that excludes audit columns for BI
consumption.
⇗ dbt — Sources, Freshness & Audit ⇗ GCP Dataplex — Data Lineage & Quality
⇗ Databricks — Delta Lake Audit Logging ⇗ Snowflake — Access History & Data Lineage
✦ Any row in the DW can be traced back to the ✔ Always — define audit columns as a DW-wide
specific source system, source record, and ETL standard at project inception, not retroactively
run that produced it
✔ Document which columns are "audit-only" vs
✦ Pipeline incidents can be diagnosed and "business attributes" in your data dictionary
reprocessed precisely — no "which batch had the
✔ Add etl_batch_id at every layer (Bronze, Silver,
bad data?" guessing
Gold in Medallion) for end-to-end lineage
✦ Data quality monitoring (late arrivals, inferred
✔ Use Dataplex, Collibra, or dbt docs to surface
member counts, correction volumes) is built-in
lineage built from these audit columns
✦ row_hash eliminates expensive column-by-
✔ Automate monitoring dashboards off load_date
column change detection in ETL — one
vs event_date to detect late-arriving data patterns
comparison per row
Description: Choosing the correct SCD type for each dimension attribute is one of the most
consequential design decisions in a DW. The wrong choice either loses history that business
needs or creates unnecessary complexity. This decision framework walks through the key
questions per attribute to arrive at the appropriate SCD type, covering all real-world scenarios
including corrections, compliance, and rapidly changing data.
Q2 Is the change a data quality correction (typo fix, format standardisation) — not a
real-world event?
→ SCD Type 1. Overwrite. Old value was simply wrong. No history needed.
Q3 Is the change a real-world event AND does the business need historical analysis
→ SCD Type 2 (default). Full version history. The standard choice for most attributes.
Q4 Is this a known one-time structural change where only the immediately prior value
matters?
→ SCD Type 3. Add prev_value column. Use only for planned migrations/reorgs.
Q5 Does the attribute change very frequently (daily/weekly for millions of rows) —
Q6 Do you need both historical accuracy AND current state accessible simultaneously
→ SCD Type 6 (Hybrid). Adds current_* columns to Type 2. Gold standard for analytics.
Q7 Is there a regulatory requirement to prove "what did the system believe at time T"
→ Bi-Temporal. Two independent time axes. Use only when compliance genuinely requires
it.
Important: SCD type is decided per attribute, not per dimension table. A single
DimCustomer may have: Type 0 for customer_since_date, Type 1 for phone_number,
Type 2 for address and segment, Type 6 for the primary analytical attributes. Always
document the SCD decision for each attribute in your data dictionary.
E-COMMERCE HEALTHCARE
⇗ Medium — Choosing the Right SCD Type ⇗ Databricks — SCD Decision Framework
✦ Eliminates the most common DW design error: ✔ Designing any new dimension table — walk
applying Type 2 to every attribute regardless of through this framework for every attribute before
business need coding
✦ Documents the reasoning behind each SCD ✔ Reviewing an existing DW for technical debt —
choice — future engineers understand why, not audit each dim attribute's SCD type against this
just what framework
✦ Prevents under-engineering (losing history that ✔ Onboarding new data engineers — this
compliance needs) and over-engineering framework replaces "just use Type 2 for everything"
(bloating with unneeded versions) as default guidance
✦ Provides a consistent framework for cross-team ✔ Record the SCD decision and rationale per
discussion and review of dimension designs attribute in the data dictionary / data catalogue
(Collibra, Dataplex)
Sheet
Dimensional Modeling Complete Reference · Based on Kimball Group methodology · Data Vault
2.0 · Modern Lakehouse patterns
Covers: Fact Types · Dimension Archetypes · SCD 0–7 · RCD · Bi-Temporal · Star/Snowflake/Galaxy/
Vault/OBT · Hierarchy Patterns · Grain · Key Strategies