Skip to content
Back to Promotions bridge
Browse Data ModelingFact table grain

Fact table grain

Declare what one row of a fact table represents, and keep every measure true to it.

7 min read

Overview

Two analysts ask the same sales table for total revenue and come back with different numbers. Neither wrote a wrong query. The disagreement is baked into the table itself, and the rows below are where it hides.

sales
order_iditemamount
101Desk lamp40
101Cable kit20
101Order total60
102Monitor arm90

The first two rows are single items from order 101, while the third is that order’s combined total. One table is describing two different kinds of thing. A fact table records the measurable events of a business and is the table you sum for revenue, an idea fact vs dimension covers in full later in this track.

The kind of thing one row stands for is called the grain. One row per order line means each row is a single item sold. One row per order means each row is a whole order. Every later decision in a fact table rests on which one you pick.

What does one row of this table represent?

The failure

The demo below runs the most ordinary query a fact table receives, a request for total revenue. The business took 150 here, since order 101 sold 60 of goods and order 102 sold 90. Before you run the sum, add up the amount column yourself and predict whether the query will report the money the business actually took.

Declare the grain and watch the total correctThe table holds four rows from two feeds. Run the sum when you have your prediction.
sales
order_iditemamount
101Desk lamp40
101Cable kit20
101Order total60
102Monitor arm90
sum(amount)
?

The query did nothing wrong. It read every row at face value and summed them, exactly as written, and it returned 210 against real revenue of 150. Order 101 entered the total twice, once through its two line rows and once through the order-total row that arrived from the second feed.

A mixed grain fails quietly because each row is individually true. The lines are real and the total is real, so no constraint can object, and every aggregate that touches the table inherits the double-count.

You can reproduce the failure with the sum itself.

select sum(amount)::int as total
from sales;

The inflated number looks plausible, and a plausible wrong number survives review. No error exists anywhere in the pipeline, so the discrepancy is usually found by a person reconciling reports long after the figure has been repeated.

The rule

Look at which rows caused the damage. The two line rows and the total row each describe order 101 truthfully, and the failure appeared only when one table held both descriptions, because a sum cannot tell which rows share meaning. A fact table therefore needs a single declared answer to the question this guide opened with, before any measure is added to it.

Dimensional modeling calls that answer the grain declaration. Ralph Kimball, whose design method shaped most modern warehouses, made declaring the grain the first step of building any fact table. The declaration comes ahead of choosing dimensions or measures, because every later choice is checked against it, and a measure that is not true at the declared grain has no honest place in the table.

The method

Repairing or designing a fact table follows the declaration through three steps.

  1. 1
    Declare the grain in one sentence

    Write the sentence "one row of this table is one order line" and treat it as part of the schema. A grain that cannot be stated in one sentence is usually two tables sharing a name.

  2. 2
    Move the wrong-grain rows out

    The order-total row belongs in a different table or nowhere, since it can be computed from the lines. When a feed delivers totals, land them separately instead of inside the line-grain table.

  3. 3
    Check every measure against the declaration

    At one row per order line, amount sums correctly and orders are counted with count(distinct order_id). A measure that only makes sense for a whole order, such as a shipping fee, either moves to an order-grain table or is allocated across the lines.

sales, one row per order line
order_iditemamount
101Desk lamp40
101Cable kit20
102Monitor arm90

The repaired table gives the opening question a single answer, and the sum from the failure section returns 150 on it. Grain repairs are cheap at design time and expensive afterward, because every downstream report inherits whatever the table declares.

The judgment

Declaring a grain forces a choice about how fine the rows should be. One row per order line is finer than one row per order, which is finer in turn than one row per day. From line-grain rows you can still compute order totals and daily totals, while a table stored at daily grain can never answer which item sold, because aggregation destroys the detail it rolls up.

The cost of fine grain is volume, since one row per order line can be many times the rows of one row per order. Kimball’s advice remains the default for exactly this trade: store the finest grain the source supports and aggregate on the way out, because storage is usually cheaper than a question you can no longer ask.

Some questions genuinely live at a coarser grain, such as an account balance measured at the end of each day. Those are served by dedicated tables built for that purpose, and they arrive later in this track as snapshot fact types.

Pitfalls

The union that mixes grains

The commonest source of a mixed table is a union of two feeds that almost match, such as line exports from one system and order exports from another. Every row loads cleanly and every value is individually true, so the corruption only becomes visible in the aggregates. When two feeds describe different grains, they belong in two tables no matter how similar their columns look.

  • A plausible total hides the damage. A double-counted sum is rarely absurd, and 210 against a true 150 survives a glance. Reconcile a fact table against its source of truth at load time, rather than trusting review to notice.
  • Order-level values on line rows multiply. A shipping fee copied onto every line of its order is summed once per line. Allocate such values across the lines or keep them in an order-grain table.
  • Counting orders from line grain needs distinct. At one row per order line, count(*) counts lines. Orders are counted with count(distinct order_id), and the difference stays invisible until an order has two lines.
  • Averages change meaning with the grain. The average of line amounts differs from the average order value. Decide which average the question asks for, and compute order totals first when it asks for the order one.

Practice

Recap

  • Declare the grain as “one row per ___” before choosing columns, using the finest event the source can provide reliably.
  • Check that every measure belongs to that row and that no table mixes order-level or line-level facts.
  • When totals grow after a join, compare row counts and keys because mixed grain and fan-out are early suspects.
  • Aggregate upward at query time and use count(distinct ...) for entities above the grain.
  • Separate the facts when a feed describes two grains instead of forcing both into one table.

Grain and fact types

Grain is the first promise a fact table makes. Before choosing columns, state what one row means. A row can be one event, one periodic observation, or one workflow instance that accumulates milestone dates over time.

Transaction facts

A transaction fact records an event that happened at a point in time. Use it when the business asks about activity, volume, revenue, or counts of atomic events.

Good fit:

  • one order line
  • one invoice line
  • one page view
  • one shipment scan

Common mistake: summarizing too early. If product managers need the event sequence, a session summary cannot replace the event fact.

Periodic snapshot facts

A periodic snapshot fact records state at a regular interval. Use it when the business asks what the balance, inventory, capacity, or population looked like at the end of each day, week, or month.

Good fit:

  • daily account balance
  • daily inventory on hand
  • monthly active subscribers
  • weekly warehouse capacity

Common mistake: storing only current state on a dimension. Current state cannot recreate historical end-of-day balances after values change.

Accumulating snapshot facts

An accumulating snapshot fact records one workflow instance and updates milestone columns as the workflow moves forward. Use it when the business asks how long work spends between stages.

Good fit:

  • one insurance claim from opened to closed
  • one order from placed to delivered
  • one support ticket from opened to resolved
  • one loan application from submitted to funded

Common mistake: splitting every milestone into disconnected facts before the workflow requires independent event analysis. If the primary question is elapsed time through a pipeline, one accumulating snapshot keeps the path visible.

The choice

Ask three questions:

  1. Is the row an event? Choose a transaction fact.
  2. Is the row a regular observation of state? Choose a periodic snapshot.
  3. Is the row a workflow instance with milestones? Choose an accumulating snapshot.

Kimball fact-table guidance is the pattern source for these choices. dbt semantic modeling reinforces the same habit from the metrics side: metrics need entities, dimensions, measures, and a grain that makes the calculation unambiguous.

Metrics-first product modeling

Product modeling starts with the question the business wants answered. Before drawing tables, define the metric, the time window, the actors, and the row grain that makes the metric computable.

Define the metric

A metric definition should name:

  • numerator
  • denominator
  • time window
  • event or state that qualifies
  • breakdown dimensions
  • exclusions and edge cases

If those pieces are vague, the schema will encode guesses. "Activation" could mean first invite sent, first invite accepted, first workspace created, or first successful collaboration. Each answer creates a different fact grain.

Choose grain from the metric

After the metric is clear, choose the lowest practical grain that supports it. If the team needs activation rate by acquisition channel and workspace plan, an atomic invite or workspace event fact can support the rate and later debugging. A user-day aggregate may be fast, but it cannot recover event sequence or changed metric definitions.

Explain tradeoffs

Interview-style prompts are not only looking for a schema. A strong answer clarifies assumptions, states grain, shows how the metric is computed, names history decisions, explains alternatives, and calls out operational concerns such as late events, backfills, bot traffic, and data quality.

Kimball transaction fact guidance is the pattern source for atomic product events. dbt semantic modeling reinforces the metrics side: entities, dimensions, measures, and time windows must line up before a metric is trustworthy.

Practice this concept

Challenges that apply what this lesson covers.