Grain-First Discipline
Declare grain before adding a single attribute. Grain is the atomic level of measurement: the finest granularity at which a fact record exists. Without grain discipline, your fact table becomes a shapeless collection of aggregations that cannot be reliably summed, drilled, or compared.
Every fact table must have exactly one declared grain. The grain specifies "one row per [X]" where [X] is a unique combination of dimension keys. Write it down. Make it non-negotiable.
ShaDey Example: FACT_DAILY_SALES
ShaDey's point-of-sale grain is declared as: one row per store + product + calendar day. This means the combination of (store_key, product_key, date_key) is unique. Every day, for every product in every store, exactly one row exists capturing that day's sales.
FACT_DAILY_SALES is sourced from VBRP (SAP billing detail). VBRP has grain of one row per invoice line item, which is finer than daily store-product. To reach daily grain, aggregate VBRP by store_key, product_key, and date_key. The aggregation is the dimensional modeling, not the source system definition.
Sample Data: FACT_DAILY_SALES
| store_key | product_key | date_key | quantity_sold | sales_amount | cost_amount | unit_price |
|---|---|---|---|---|---|---|
| 1001 | 5001 | 20260315 | 24 | 576.00 | 288.00 | 24.00 |
| 1001 | 5002 | 20260315 | 18 | 450.00 | 180.00 | 25.00 |
| 1002 | 5001 | 20260315 | 31 | 744.00 | 372.00 | 24.00 |
| 1001 | 5001 | 20260316 | 22 | 528.00 | 264.00 | 24.00 |
| 1003 | 5003 | 20260315 | 15 | 390.00 | 150.00 | 26.00 |
The Mixed-Grain Anti-Pattern
A common mistake: mixing order header and order-line grain in one fact table. An order header has attributes like order date and customer. An order-line has product and quantity. When you put both in FACT_SALES, the order-date attribute replicates on every line, and the unit-price becomes non-additive. Do not do this.
If you SUM a measure in a fact table and the result does not reconcile to the source system, you likely have mixed grains. The table contains multiple atomic levels masquerading as one.
SQL Grain Verification
To verify that your fact table maintains grain discipline, run a grain-check query: the primary key count must equal the total row count.
- VBRK (SAP billing header) - has one row per invoice
- VBRP (SAP billing line item) - has one row per line item in an invoice
- LIPS (SAP delivery line) - one row per delivery line
- KNA1 (SAP customer master) - one row per customer
- MARA (SAP material master) - one row per material (product)
Answer: For a retail sales fact table sourced from VBRP, the grain is one row per invoice-line-item. To get to daily store-product, you must aggregate VBRP with date grouping.
Detecting Hidden Mixed Grains
Mixed grains hide in plain sight. Your fact table passes schema validation, but when analysts query it, aggregation totals don't match the source system. This section teaches you how to detect and diagnose hidden mixed-grain problems.
The Promotion Fact Symptom
ShaDey's promotion team sources promo facts from two systems: SAP (store-level allocation) and the POS system (item-level redemption). A well-meaning engineer merges these into FACT_PROMOTION without declaring grain. The result: store + product + date + promotion_id appears unique, but store-level allocation rows co-exist with item-level redemption rows at different granularities.
Sample Data Showing the Problem
| store_key | product_key | date_key | promo_id | allocated_budget | redeemed_amount |
|---|---|---|---|---|---|
| 1001 | NULL | 20260315 | PROMO_001 | 1000.00 | 0.00 |
| 1001 | 5001 | 20260315 | PROMO_001 | 0.00 | 250.50 |
| 1001 | 5002 | 20260315 | PROMO_001 | 0.00 | 175.25 |
Notice: store 1001 on 2026-03-15 has allocated_budget=1000 (one row), but redeemed_amount spreads across multiple product rows. Summing allocated_budget across products gives 1000, but that's not additive per product. This is mixed grain.
Mixed grain reveals itself through reconciliation failures. If your FACT_PROMOTION.allocated_budget doesn't match SAP budget totals when grouped by store-promotion-date, you have mixed grain. Always test: "Does SUM(fact_measure) GROUP BY [declared grain keys] match the source system total?" If not, grain is violated.
The Fix: Grain Separation
Create two separate fact tables: one for allocation (grain: store-date-promotion), one for redemption (grain: store-product-date-promotion).
ShaDey runs promotions across stores via SAP VBAK/VBAP. Allocation budgets come from SAP allocation tables (store-promotion-date grain), while redemptions come from POS (store-product-date-promotion grain). These are separated into FACT_PROMO_ALLOCATION and FACT_PROMO_REDEMPTION. Never merge them.
Three Fact Table Types
There are three archetypes for fact tables in dimensional modeling. Each has its own grain, use case, and aggregation semantics. Understanding which to use prevents fundamental design errors.
Type 1: Transaction Facts
One row per atomic transaction event. Lowest grain, immutable after creation, additive at transaction level. Most common for POS and operational analytics.
Type 2: Periodic Snapshot Facts
One row per dimension combination per time period. Grain declared explicitly (e.g., store-product-week). Semi-additive measures (inventory_on_hand). Used for inventory, aging analysis.
Type 3: Accumulating Snapshot Facts
One row per order that persists and updates through fulfillment lifecycle. Grain: one row per order. Multiple date keys mark each milestone. Used for order tracking and SLA monitoring.
Comparison Table: When to Use Each Type
| Fact Type | Grain | Immutable? | Best For | Example |
|---|---|---|---|---|
| Transaction | Event | Yes | Point-of-sale, payments, clicks | FACT_POS_SALES |
| Periodic Snapshot | Dimension combo + period | No | Inventory, account balance, aging | FACT_WEEKLY_INVENTORY |
| Accumulating Snapshot | Process instance | No | Order fulfillment, claim workflow | FACT_ORDER_FULFILLMENT |
At ShaDey, choose your fact table type based on the business process: FACT_SALES is transactional (one row per invoice line from VBRP), FACT_WEEKLY_INVENTORY is periodic snapshot (one row per sku-store-week), FACT_ORDER_FULFILLMENT is accumulating snapshot (one row per order tracking order-date, ship-date, delivery-date columns). Each type has different querying patterns and aggregation rules.
Additivity Traps
Not all measures in a fact table can be summed across all dimensions. Additivity is a contract between the fact table designer and the analyst. Violate it, and your BI tool will produce nonsensical results that pass all syntax checks.
Additivity Types
Fully Additive Measures
Safe to SUM across all dimensions. Examples: quantity_sold, sales_amount, discount_amount. These are counts or amounts that accumulate naturally.
Semi-Additive Measures
Can be summed across some dimensions, but NOT time. Example: inventory_on_hand. You can sum inventory across products for a store on a date, but you CANNOT sum inventory across dates (that makes no sense: you'd add Monday's inventory + Tuesday's inventory, which is double-counting). Use AVERAGE or MAX across time instead.
Non-Additive Measures
Never SUM. Examples: unit_price, margin_percentage, average_order_value. These are ratios or unit metrics. Always use AVERAGE or weighted aggregations.
Document every measure with its additivity type. BI tools cannot detect additivity violations. A wrong aggregation query returns a plausible-looking number that is completely wrong.
ShaDey Additivity Reference Table
| Measure | Additivity | Safe Dimensions | Danger Dimension | Correct Aggregation |
|---|---|---|---|---|
| sales_amount | Fully Additive | All | None | SUM |
| quantity_sold | Fully Additive | All | None | SUM |
| unit_price | Non-Additive | None | All | AVERAGE or WEIGHTED_AVG |
| inventory_on_hand | Semi-Additive | Product, Store | Time | SUM by product/store, AVERAGE by date |
| margin_percentage | Non-Additive | None | All | (SUM profit) / (SUM cost) |
SQL Example: Correct vs. Incorrect
Every measure in ShaDey fact tables must have documented additivity. FACT_SALES documents: quantity_sold (fully additive), sales_amount (fully additive), unit_price (non-additive - use AVG), discount_percent (non-additive - use AVG), gross_margin (non-additive - use weighted AVG). Place these rules in the data dictionary and ETL documentation.
Multi-Calendar Time Modeling
Retail operates in multiple calendar systems simultaneously: calendar year (Jan-Dec), fiscal year (varies by company), and NRF 4-5-4 retail calendar (weeks grouped into 4, 5, or 4-week periods). A single DIM_DATE must support all three without redundancy.
ShaDey Fiscal Calendar
ShaDey's fiscal year runs Feb-Jan (aligned with global retail conventions). Each fiscal year is divided into 13 four-week periods, plus the fiscal year adjustment week. DIM_DATE must map every calendar day to all three calendar systems.
Sample Data: 2026-03-15 Across All Calendars
| date_key | calendar_date | calendar_year | fiscal_year | fiscal_period | nrf_week | nrf_period |
|---|---|---|---|---|---|---|
| 20260315 | 2026-03-15 | 2026 | 2026 | 2 | 11 | 2 |
Store all calendar mappings in DIM_DATE. Do NOT compute fiscal_year in BI queries. Let DIM_DATE be the single source of truth. This prevents calendar disputes between Finance and Operations.
Late-Arriving Facts & Dimensions
The real world doesn't respect ETL schedules. A POS sale is recorded 3 days after transaction. A product is reclassified after being loaded. A store opens but the dimension hasn't been refreshed yet. This section covers patterns for handling asynchronous data arrival.
Late-Arriving Dimension Pattern: Inferred Members
When a fact references a dimension key that doesn't exist yet, create an inferred member with a placeholder description. When the dimension is loaded later, backfill the inferred member or update the fact key.
At ShaDey, inferred members (product_key=-999, store_key=-999) are used to capture facts that arrive before their dimension records are loaded. These placeholders are later reconciled when the real dimension rows arrive. Use is_inferred flag to track which dimension members are awaiting backfill from SAP MARA, KNA1, or MARD master data.
SCD Type Tradeoffs in Multi-Source
Slowly Changing Dimension handling gets complex when facts come from multiple source systems with different governance. SAP demands Type 2 (history), CRM demands Type 1 (overwrite). This section resolves the tension.
SCD Type 1: Overwrite (Type 1)
Update dimension attributes in place. Fast, simple, loses history. Use for low-cardinality attributes (customer country) where history is never needed.
SCD Type 2: Versioned (Type 2)
Insert new row for every change. Preserves complete history. Add row_effective_date, row_expiration_date, and is_current_row columns. Requires carefully filtering in BI queries.
SCD Type 3: Previous Value
Keep only current and previous values. Store current_value and previous_value columns. Good for "before-after" analysis without full versioning overhead.
DIM_CUSTOMER uses Type 2 because customer address/segment history is essential for regional and cohort analysis. DIM_PRODUCT uses Type 2 because product category changes affect category-year comparisons. DIM_STORE uses Type 1 because store attributes rarely change and historical store changes are never analyzed.
Decision Matrix: Which SCD Type for ShaDey?
| Entity | Change Frequency | History Needed? | Recommended SCD Type | Reason |
|---|---|---|---|---|
| Customer | Low | Yes | Type 2 | Address changes matter for regional analysis |
| Product | Medium | Yes | Type 2 | Category changes affect historical reporting |
| Store | Very Low | No | Type 1 | Store attributes rarely change; ignore history |
Filter Behavior vs SCD Behavior
BI tools (Tableau, Power BI, Looker) each handle SCD2 dimensions differently. Some filter to current row only, others apply as-of-date logic automatically. Misunderstanding this causes incorrect drill-down behavior and silent data errors.
Tableau uses as-of-date filtering (rows active on fact date). Power BI requires explicit relationship filters. Looker uses LEFT JOIN semantics. Design your DIM_CUSTOMER.is_current_row flag to handle all three.
Safe vs Unsafe Drill Paths with SCD2
| Drill Path | Safe? | Issue | Solution |
|---|---|---|---|
| Fiscal Quarter -> Day | Yes | None | Drill naturally within timeline |
| Customer Region -> Customer Name | No | If customer moves regions, drill breaks | Use is_current_row flag; filter drill explicitly |
| Product Category -> Product SKU | No | Historical sales show under new category | Use as-of-date in drill query |
Conformed Dimensions
Conformed dimensions are shared dimension tables used identically across multiple fact tables. They enable cross-process analytics by ensuring that when you join FACT_SALES and FACT_INVENTORY through DIM_PRODUCT, you're analyzing the same product definition. Kimball's bus matrix documents conformance.
Definition & Purpose
A dimension is "conformed" when it is used unchanged across multiple fact tables. For example, DIM_DATE used by FACT_SALES, FACT_INVENTORY_SNAPSHOT, FACT_ORDER, and FACT_RETURN must have identical calendar logic, grain, and surrogate keys. Conformance prevents "same metric, different answer" problems that plague disconnected data warehouses.
The bus matrix is a Kimball technique that documents which conformed dimensions apply to which fact tables. For ShaDey, DIM_DATE, DIM_PRODUCT, and DIM_STORE are fully conformed across all four major fact tables: FACT_SALES, FACT_INVENTORY_SNAPSHOT, FACT_ORDER, and FACT_RETURN. Non-conformed dimensions (e.g., FACT_SALES has DIM_PROMOTION but FACT_ORDER does not) break the bus.
ShaDey Inc. Conformed Dimension Examples
ShaDey operates three fully conformed dimensions that align across the retail 8 business domains (Ordering, Fulfillment, Returns, Inventory, Promotions, Customers, Products, Stores):
- DIM_DATE: Fiscal calendar with year, quarter, month, week, day, weekday, holiday_flag. Used by FACT_SALES (transaction date), FACT_INVENTORY_SNAPSHOT (snapshot date), FACT_ORDER (order date), FACT_RETURN (return date). SAP source: VBAK-ERDAT, VBRK-BLDAT.
- DIM_PRODUCT: Material master from SAP MARA (MATNR, MTART, SPART, BISMT, MEINS). Conformed across FACT_SALES (product sold), FACT_INVENTORY_SNAPSHOT (sku held), FACT_ORDER (product ordered), FACT_RETURN (returned sku).
- DIM_STORE: Plant/warehouse master from SAP T001W (plant table, keyed by WERKS). Includes location, region, warehouse code. Conformed across FACT_SALES (selling location), FACT_INVENTORY_SNAPSHOT (holding location), FACT_RETURN (return location).
Bus Matrix: ShaDey Fact & Dimension Conformance
| Fact Table | Source SAP | Grain | DIM_DATE | DIM_PRODUCT | DIM_STORE | DIM_CUSTOMER | Other Dims |
|---|---|---|---|---|---|---|---|
| FACT_SALES | VBRK/VBRP (Invoice) | One row per invoice line | Yes (invoice_date) | Yes (material) | Yes (shipping location) | Yes (bill-to customer) | DIM_PROMOTION, DIM_SALESPERSON |
| FACT_INVENTORY_SNAPSHOT | MARD (Stock) | One row per SKU per warehouse per day | Yes (snapshot_date) | Yes (material) | Yes (warehouse WERKS) | No | DIM_BATCH_STATUS |
| FACT_ORDER | VBAK/VBAP (Sales Order) | One row per order line | Yes (order_date) | Yes (material ordered) | Yes (ship-to location) | Yes (sold-to customer) | DIM_DELIVERY_STATUS |
| FACT_RETURN | VBRK/VBRP (Credit Memo) | One row per return line | Yes (return_date) | Yes (material returned) | Yes (return location) | Yes (returning customer) | DIM_RETURN_REASON |
Because DIM_DATE, DIM_PRODUCT, and DIM_STORE are conformed, you can write a single drill path: "Sales by Product -> Inventory on Hand -> Orders Pending -> Returns This Year" without worrying about conflicting date definitions or product hierarchies. Non-conformed dimensions break this trust.
Conformance Validation: ShaDey PDM Tables
In ShaDey's PDM (Persistent Data Model), conformance is enforced through shared reference tables. All fact tables reference the same DIM_DATE.date_key, same DIM_PRODUCT.product_key, and same DIM_STORE.store_key. This is not a recommendation; it is a non-negotiable requirement for dimensional design at ShaDey.
Without conformed dimensions, combining FACT_SALES and FACT_INVENTORY requires reconciliation logic between different date definitions. With conformed dimensions, the join is trivial and auditable. Every analyst gets the same "Product X sold in Region Y" number because they all use the same DIM_PRODUCT and DIM_STORE.
Junk & Mini-Dimensions
Junk dimensions combine low-cardinality flags into a single dimension table to reduce fact table width. Mini-dimensions extract frequently-changing attributes from slowly-changing dimensions to avoid SCD2 bloat and improve query performance.
Junk Dimensions: Combining Flags
Every fact table accumulates boolean and low-cardinality flags: is_gift_wrapped, is_express_shipping, is_loyalty_member, is_returns_eligible, is_same_day_delivery. Adding these as separate columns wastes storage and query performance. Instead, create a junk dimension that combines them into a single surrogate key.
A junk dimension stores all combinations of low-cardinality flags in a single table. For example, DIM_ORDER_FLAGS combines 6 boolean columns (gift_wrapped, express, loyalty, refundable, same_day, corporate). This creates at most 2^6 = 64 rows. Instead of 6 BOOLEAN columns in FACT_SALES, you add one order_flags_key INT. This reduces fact table width by 5 bytes per row (40 bytes at scale).
ShaDey Example: DIM_ORDER_FLAGS
FACT_ORDER currently stores six separate boolean columns. These flags come from SAP VBAK (sales order header) and VBAP (sales order item). Consolidating them into DIM_ORDER_FLAGS:
Reduces fact table width (saves storage and I/O). Makes filtering cleaner (one join vs. WHERE clause on 6 columns). Enables centralized management of flag semantics. At ShaDey scale (billions of orders), this saves terabytes of storage.
Mini-Dimensions: Extracting Volatile Attributes
Some dimension attributes change frequently, causing SCD2 to create excessive versions. For example, DIM_CUSTOMER stores both stable attributes (customer_id, legal_name, country) and volatile attributes (annual_credit_limit, customer_segment, loyalty_points). A customer's credit limit might change 10 times per year, creating 10 new DIM_CUSTOMER rows (SCD2 bloat). Instead, extract the volatile attributes into a separate mini-dimension.
A mini-dimension is a separate table that stores frequently-changing attributes with its own SCD2 history. DIM_CUSTOMER_DEMOGRAPHICS stores credit_limit, customer_segment, loyalty_tier, and has its own row_effective_date and row_expiration_date. DIM_CUSTOMER stays lean (stable attributes only) and joins to DIM_CUSTOMER_DEMOGRAPHICS via customer_key + effective_date logic.
ShaDey Example: Separating DIM_CUSTOMER from DIM_CUSTOMER_DEMOGRAPHICS
ShaDey's customer master (SAP KNA1) includes both stable fields (KUNNR, NAME1, LAND1) and volatile fields (EIKTO credit limit, KTOKD account group). For reporting, volatile attributes cause DIM_CUSTOMER SCD2 bloat. Solution: create a mini-dimension for demographics.
Prevents SCD2 bloat on DIM_CUSTOMER (which could have millions of versions). Allows different SCD strategies (DIM_CUSTOMER is SCD1, DIM_CUSTOMER_DEMOGRAPHICS is SCD2). Improves query performance (DIM_CUSTOMER is smaller, more cacheable). Makes reporting logic clearer (segment is explicitly demographic, not core customer identity).
Business Keys vs Technical Keys
Every entity has a business key (SAP material number, customer account number) and a technical key (surrogate integer, hash). They serve different purposes. Confusing them breaks reconciliation.
Business Key: The Stable Identifier
A business key is how humans and source systems refer to an entity. For ShaDey: MATNR (SAP material), KUNNR (SAP customer), WERKS (SAP plant/store). Business keys must be stable across time and systems. They should never be reused for a different entity.
Technical Key: The Database Identifier
A surrogate integer (or hash) used for storage efficiency and join performance. Examples: product_key=5001, customer_key=10234. Technical keys are internal; never expose them to business users or source systems.
Key Stability Audit: ShaDey Entities
| Entity | Business Key (SAP) | Stable? | Reused? | Audit Status |
|---|---|---|---|---|
| Product | MATNR | Yes | No | Pass |
| Customer | KUNNR | Yes | Occasionally (inactive reused) | Flag for validation |
| Store | WERKS | Yes | No | Pass |
All ShaDey dimensions use MATNR (product), KUNNR (customer), and WERKS (store) as business keys sourced from SAP. Technical keys (product_key INT, customer_key INT, store_key INT) are auto-generated surrogates. Facts always join on technical keys. Reporting and audits always validate against business keys to catch reuse or stability issues.
Canonical Entity Design
A canonical entity is a minimal, stable definition of a business concept. It does NOT include every possible attribute. It defines the core identity that other systems agree on. Extensions live in separate satellite tables.
Why Canonical Design Matters
A canonical entity defines the narrowest useful core: the minimal set of attributes that ALL consumers need. By separating core from extended, you achieve:
- Data stability: The core table rarely changes; only new attributes trigger extended table updates
- Multi-consumer alignment: Finance, supply chain, and merchandising all join to the same DIM_PRODUCT_CORE and know they're seeing the "official" product identity
- Graceful extension: New domain-specific attributes (e.g., planogram position for retail, lead time for supply chain) join via satellite tables without impacting existing queries
- Performance: Smaller core tables scan faster for daily operational lookups; extended attributes are only hydrated when needed
Canonical Core vs Extended Attributes: DIM_PRODUCT Example
At ShaDey Inc., the product dimension is consumed by sales, supply chain, and merchandising teams. The canonical core is what they all agree on; extensions vary by domain:
| Attribute | Category | Value (Example: Denim Jacket XL Blue) | SAP Source | Consumers |
|---|---|---|---|---|
| product_key | CORE | 4521928 | MARA.MATNR | All |
| product_id | CORE | MAT-10847392 | MARA.MATNR | All |
| product_name | CORE | Denim Jacket XL Blue | MARA.MAKTX | All |
| category | CORE | Outerwear / Jackets | MARA.MATKL / T179 hierarchy | All |
| brand | CORE | ShaDey Premium | MARA.BRAND (custom field) | All |
| planogram_position | EXT | Shelf A, Tier 2 | SCML external table | Merchandising |
| shelf_life_days | EXT | 365 | MARC.LMZFL | Merchandising, QA |
| lead_time_days | EXT | 45 | MARC.BESCHAFFUNGSZEIT | Supply Chain, Procurement |
| reorder_point_qty | EXT | 150 units | MARC.EISBE | Supply Chain, Procurement |
| minimum_order_qty | EXT | 12 units | MARC.BSTME | Supply Chain, Procurement |
Reference Data Harmonization Across Multi-System Sources
SAP, Salesforce, and Shopify each define reference codes (e.g., country, currency, unit of measure) differently. The DWH must harmonize these into a single canonical representation:
Canonical Entity Design Rules at ShaDey
- Rule 1: Minimal Core: Include only attributes that appear in 80% or more of use cases. If an attribute is domain-specific, it belongs in an extension table.
- Rule 2: No Overwriting: Once a core attribute is defined, never remove it or change its data type. Extensions can be added; the core is append-only.
- Rule 3: Single Source per Attribute: Each core attribute maps to exactly one source system or derivation rule. Avoid blending SAP + Salesforce in the same column; create separate columns and document precedence.
- Rule 4: Reference Harmonization: All code tables (country, currency, UoM, status) must have a canonical definition in the DWH with mappings to source systems. No ad-hoc lookups.
The canonical core is not the union of all attributes across all consumers. It is the intersection of what they all need, plus what rarely changes. Everything else belongs in satellites. This design principle prevents the DWH dimension from becoming a sprawling, fragile, slow-to-query monster table.
Reference Data Survivorship: When Systems Disagree on Definitions
When SAP uses country code "US", Salesforce uses "United States", and Shopify uses "USA", the data warehouse must harmonize these into a single canonical definition. Reference data survivorship requires identifying the master source for each reference domain and mapping all others to it.
ShaDey Example: Order #4500001234 is placed from "United States" (Shopify). When it lands in SAP, it arrives as LAND1="US". The DIM_COUNTRY lookup must translate both Shopify "United States" and SAP "US" to a canonical ISO code "US" (ISO 3166-1) so the order joins correctly to dimensional queries.
Reference Data Governance: Who Owns the Mapping?
- Master Source Selection: ISO country codes are international standards. All source systems map to ISO. If SAP adds a new country before Shopify, the ETL still maps it correctly via DIM_COUNTRY.
- Mapping Maintenance: When Shopify changes how it represents "United Kingdom" (UK → GB), the mapping is updated once in DIM_COUNTRY. All downstream queries automatically use the new canonical value.
- New Code Handling: If a source system introduces an unmapped code (e.g., Shopify adds "Palestine" as "PS"), ETL should flag it as "UNMAPPED_CODE" and require manual resolution before loading. Never silently null out unmapped codes.
- Validation Query: Run a periodic audit to find source values with no DIM_COUNTRY mapping. This catches upstream schema changes or new enum values.
Rule 2: Document Precedence: If Salesforce and SAP disagree on which customer is in which region, document the tie-breaker rule. Example: "SAP LAND1 is authoritative for tax/billing; Salesforce is advisory for marketing."
Rule 3: Versioned Mappings: When a mapping changes (Shopify swaps UK to GB), timestamp the change. Queries can ask "Show 2024 orders by country using 2024 mappings" vs. "Show 2024 orders using today's mappings."
ShaDey Reference Data: Standardized Across All Systems
ShaDey Inc. Technical Key Strategy: Hash (MD5)
ShaDey uses MD5 hashing of the business key for all dimensions. This enables load-order-independent reconciliation: if two ETL runs load the same SAP MATNR, they generate the same product_key hash. The approach supports parallel source ingestion (SAP, CRM, POS) without coordination.
When to Use Each Strategy
Sequence (Auto-increment): Use when ETL is single-threaded, centralized, and load order is predictable. Common in traditional data warehouses with nightly batch loads from one source. Trade-off: Not reproducible across reloads.
Hash: Use for deterministic, reproducible keys in modern ELT/cloud environments. Data Vault methodology mandates hash keys. Enables parallel independent loads (SAP loads to table A, CRM loads to table B, both generating same hash keys). Trade-off: Hash collisions extremely rare but require explicit handling; keys are larger (16-32 bytes vs. 8).
UUID/GUID: Use in highly distributed systems (microservices, federated warehouses) where no central coordination exists. Trade-off: Large (16 bytes), unordered, not sortable for pagination or range queries.
Range Allocation: Use when multiple source systems feed the warehouse and you want to avoid hash computation overhead. Pre-allocate key ranges: SAP gets 1-1,000,000; CRM gets 2,000,000-3,000,000. Each source team owns their range. Trade-off: Requires centralized governance; source changes require reallocation.
ShaDey chose hash keys because: (1) reproducibility across partial reloads, (2) parallel safe for multi-source ingestion, (3) easy reconciliation across environments (dev/staging/prod generate identical keys). If ShaDey's ETL were strictly sequential and never reloaded, sequences would be simpler. If ShaDey were globally distributed with 100 independent data teams, UUIDs would be mandatory.
BI Consumption Safety Patterns
A well-designed star schema can still cause BI mishaps. This section covers patterns that make fact tables safe for analyst self-service without requiring a data engineer to validate every query.
Semantic Clarity: Naming Conventions
Use consistent, unambiguous column naming. Prefix dimensions with _key, measures with _amount or _quantity, flags with is_ or has_.
| Pattern | Meaning | Example | Anti-Pattern |
|---|---|---|---|
| *_key | Foreign key to dimension | store_key, product_key | store_id, product |
| *_amount | Monetary measure | sales_amount, discount_amount | sales, discount |
| *_quantity | Unit count | quantity_sold, units_received | qty, count |
| is_* | Boolean flag | is_weekend, is_holiday | weekend_flag, holiday |
Aggregate Awareness Pattern
Create pre-aggregated fact tables for common queries (daily, monthly). Tag dimensions as "denormalized for query speed" vs. "core reference." Use conformed dimensions across all aggregations.
Drill-Path Safety
Avoid fan-out at fine grains. If analyst drills from quarterly summary to daily detail on a many-to-many relationship (product category to transaction), results become unreliable. Always drill along conformed dimension hierarchies.
All ShaDey BI models enforce strict naming conventions (_key, _amount, _quantity, is_*), use conformed dimensions (DIM_DATE, DIM_PRODUCT, DIM_STORE), and document drill paths that are safe. Analysts cannot drill from FACT_SALES (daily transaction grain) to a product-store-category hierarchy and sum; they must use pre-aggregated FACT_MONTHLY_SALES or explicitly apply grain-aware filtering.
Neo4j Graph for Dimensional Model Lineage
Model your entire star schema as a graph in Neo4j to track lineage: which sources feed which facts, which dimensions are shared, which transformations apply. This section provides complete Cypher code.
Graph Model: Nodes and Relationships
Nodes: Fact tables, Dimension tables, Source tables, Attributes, Transformations. Relationships: FEEDS_INTO, SHARED_BY, ATTRIBUTES, TRANSFORMS.
Query: Shared Dimensions
A Neo4j lineage graph enables impact analysis: "If I change DIM_PRODUCT category hierarchy, which facts are affected?" This prevents silent data quality issues from cascading across reports.