Data Warehousing

How to Design Fact and Dimension Tables That Analysts Love

Every analytics team has encountered the same downstream nightmare: a data warehouse packed with sprawling, undocumented tables where a single query requires ten-table joins, nested window functions,

Published 10/24/2026 · Izan Technologies

Want this built for your team?

Senior engineers, your cloud, your IP.

Every analytics team has encountered the same downstream nightmare: a data warehouse packed with sprawling, undocumented tables where a single query requires ten-table joins, nested window functions, and guesswork around which date column represents the actual event. The result is bloated query costs, sluggish dashboards in Power BI or Tableau, and endless Slack messages asking, "Which field should I use for revenue?"

In modern data warehousing, compute engines like Snowflake, Databricks, AWS Redshift, and Azure Synapse are fast enough to mask poor architecture for a while. But brute-force compute cannot solve architectural ambiguity. To empower data analysts, BI developers, and ML engineers, you must design fact and dimension tables that prioritize intuitive discovery, consistent grains, and predictable join patterns.

Here is how to design fact dimension structures that eliminate ambiguity, accelerate query response times, and turn your analytical data layer into an asset analysts actually enjoy using.

1. Establish the Uncompromising Rule: Declare the Grain

The cardinal sin of dimensional modeling is building a fact table without explicitly defining its grain. The grain represents the exact real-world business event or snapshot that a single row in the fact table represents.

When engineers skip this step or mix grains—such as combining line-item detail and invoice-level order totals in a single table—analysts inevitably double-count metrics.

Before writing a single DDL or dbt model, document the grain in plain language:

  • "One row per item sold per retail transaction." (Transaction Fact)
  • "One row per account balance at 23:59:59 UTC each calendar day." (Periodic Snapshot Fact)
  • "One row per insurance claim lifecycle state change." (Accumulating Snapshot Fact)

If an analyst needs both item-level revenue and overall order shipping fees, design two separate fact tables or allocate the order-level metric down to the line item using a deterministic distribution key. Never leave metric allocation or grain deduplication to an analyst writing a SQL report.

2. Structure Fact Tables for Predictable Aggregation

Fact tables contain the numeric measurements generated by business processes, flanked by foreign keys pointing to dimension tables. To make these tables painless for analysts, your data modeling should follow three core technical standards.

Differentiate Metric Types Clearly

Analysts run aggregations across dimensions. Classify and name your metrics so users instantly know their aggregation behavior:

  • Fully Additive: Measures that can be summed across any dimension (e.g., quantity_sold, extended_gross_amount_usd). These are the safest and most intuitive for self-service BI.
  • Semi-Additive: Measures that can be summed across some dimensions (like geography or customer segment) but not time (e.g., closing_account_balance, current_inventory_on_hand). Mark these clearly in documentation and data dictionaries.
  • Non-Additive: Ratios, percentages, and unit prices (e.g., margin_pct, unit_discount_rate). Never store pre-calculated ratios in base fact tables without also storing the underlying numerator and denominator. Store gross_profit_usd and gross_revenue_usd so BI tools can compute SUM(gross_profit_usd) / SUM(gross_revenue_usd) dynamically across filtered slices.

Handle Degenerate Dimensions Thoughtfully

Identifier keys like order_number, invoice_id, or tracking_number do not belong in their own dimension table if they carry no descriptive context beyond their value. Store them directly in the fact table as degenerate dimensions. They give analysts an anchor to reconcile metrics against source operational systems without requiring an unnecessary join to an empty dimension.

Eliminate Nulls in Foreign Keys

A foreign key column in a fact table should never be NULL. If an event occurs where a dimension attribute is unknown or not yet applicable (for example, a web order placed without an assigned sales_rep_id), map that foreign key to a surrogate key of -1 (representing "Unknown") or -2 (representing "Not Applicable") in the dimension. This preserves simple inner joins and avoids outer joins that complicate downstream SQL and degrade execution plans.

3. Engineer Rich, Denormalized Dimension Tables

While operational systems use 3NF (Third Normal Form) to minimize write redundancy, analytical data warehouses thrive on flat, context-rich dimensions. Analysts should rarely have to traverse snowflake-style chains (e.g., dim_store -> dim_city -> dim_state -> dim_country) to answer an executive question.

[Operational (3NF)]          [Analyst-Ready (Star Schema)]
Store -> City -> Region  ===>  dim_store (store_name, city_name, region_name)

Denormalize the hierarchy directly into the primary dimension. The storage cost of a few duplicate text strings in Snowflake or Delta Lake is negligible compared to the cognitive friction and join overhead placed on your analytics team.

Fact vs. Dimension Architectural Differences

When you design fact dimension tables, keep their core functional profiles separated:

Design AttributeFact TableDimension Table
Primary PurposeRecords measurements of business eventsProvides context, filtering, and grouping attributes
Row CardinalityHigh to extreme (millions to billions of rows)Low to medium (thousands to millions of rows)
Column WidthNarrow (mostly foreign keys and numeric metrics)Wide (dozens of descriptive, textual attributes)
Key StrategyComposite business grain + surrogate foreign keysSingle Surrogate Primary Key (e.g., customer_sk)
Update PatternsAppend-mostly; partition-level drops/replacesSCD Type 1 (in-place update) or SCD Type 2 (versioned)
Null StrategyNo null foreign keys (use -1 placeholders)Standard SQL nulls allowed for missing attributes

Managing Slowly Changing Dimensions (SCD)

Do not force SCD Type 2 tracking (creating a new row with valid-from and valid-to timestamps every time an attribute updates) across every column. Overusing SCD Type 2 explodes row counts and requires analysts to write complex point-in-time join conditions (JOIN ON fact.date BETWEEN dim.valid_from AND dim.valid_to).

  • Use SCD Type 1 (Overwrites) for attributes where historical drift doesn't matter (e.g., correcting a customer's misspelled name or updating a phone number).
  • Reserve SCD Type 2 (Row Versioning) for critical reporting hierarchies (e.g., territory realignment or sales rep department shifts).
  • When implementing SCD Type 2, always maintain a boolean flag is_current = TRUE and an effective_end_date set to a far-future sentinel value (like 9999-12-31). This allows analysts to filter for current state with a simple, high-performance boolean condition.

4. Modern Warehouse Optimizations: Beyond Kimball

Classical Kimball modeling remains the clearest structural framework for analytics, but modern columnar storage layers introduce architectural nuances you must address.

Surrogate Keys in the Cloud Era

Traditional auto-incrementing identity columns can create concurrency bottlenecks during parallel distributed loads in platforms like Databricks or Snowflake. Instead, construct deterministic surrogate keys using cryptographic hashes (such as MD5 or SHA256) calculated from the business key and, if applicable, the SCD version timestamp:

-- dbt / Databricks SQL example
MD5(CONCAT_WS('||', COALESCE(customer_id, ''), COALESCE(CAST(valid_from AS STRING), ''))) AS customer_sk

This guarantees deterministic, idempotent pipelines across development and production environments without table locks.

Physical Optimization: Clustering and Micro-Partitioning

Analysts write queries driven by specific filters and groupings. Match your warehouse's physical clustering to those query habits:

  • In Snowflake, avoid clustering small tables (<1TB). For large fact tables, cluster on the primary join key and date dimension (e.g., CLUSTER BY (transaction_date, customer_sk)).
  • In Databricks, utilize Liquid Clustering (CLUSTER BY (event_date, customer_id)) over Delta tables rather than legacy manual partitioning schemes, reducing small-file skew and optimizing partition pruning.

Build Maintainable Data Products with Izan Technologies

Designing fact and dimension tables that scale seamlessly between human analysts and machine learning pipelines requires deep technical domain experience. At Izan Technologies, we design and implement enterprise data engineering, cloud data warehousing, and AI architectures across AWS, Azure, Snowflake, and Databricks.

Unlike conventional agencies that lock you into proprietary platforms or hold codebase access, Izan Technologies operates under a complete transparency model: our clients own 100% of the intellectual property, code, and architectural assets we create.

Whether you need to modernize a legacy relational data warehouse into an automated dbt-driven star schema, optimize your cloud warehouse compute costs, or build real-time feature stores for machine learning, our engineering teams build scalable, production-ready systems tailored to your business needs.

How Izan Technologies Helps

Transform your data architecture from a bottleneck into a competitive advantage:

Want this built for your team?

Senior engineers, your cloud, your IP.