What does it mean to declare the grain of a fact table before choosing its dimensions?
answer
- one row per what?
- a sentence a business person can reject
- decided before columns are chosen
- it makes dimensions and measures testable
- and it is checkable with a uniqueness query
basics
~20 sDeclaring the grain states in one business sentence what a single fact row represents, such as one row per order line. It is fixed before dimensions and measures, because both are only valid at exactly that level.
solid answer
~50 sThe grain is a one-sentence statement of what exactly one row of the fact table means — "one row per product scanned on one POS ticket", "one row per policy per month-end". Declaring it first turns the rest of the design into checkable tests rather than opinions: a dimension qualifies only if it takes exactly **one** value for a row at that grain, and a measure qualifies only if it is genuinely measured at that level. Everything downstream depends on it — which questions the table can answer, whether `SUM` is safe, and what a duplicate even means. State it in business language, not as a list of key columns, so a domain expert can confirm or reject it. Then enforce it: a uniqueness check on the columns that define the grain is the test that catches violations before a report does.
code
sql · 5 lines-- Grain: one row per order line. This must return zero rows.
SELECT order_id, line_number, COUNT(*) AS row_count
FROM fact_order_line
GROUP BY order_id, line_number
HAVING COUNT(*) > 1;go deeper
Be able to say what grain means and give one example sentence, such as one row per order line. Interviewers often ask for it as the very first design question.
Explain the two tests grain enables — a dimension must take one value per row, a measure must be taken at that level — and why it is declared before either is chosen.
Demonstrate enforcement: the uniqueness query that proves the declaration, the symptoms of a violated grain in production reports, and why you rebuild rather than patch when the business grain genuinely changes.
Own grain as a published contract between producers and consumers: every fact table documents one sentence, changes to it are treated as breaking, and detection is automated rather than discovered by an analyst.
## What grain actually is The grain of a fact table is the answer to one question: *what does a single row of this table represent?* A good answer is a short declarative sentence in the language of the business. - "One row per product scanned on one point-of-sale ticket." - "One row per policy per month-end." - "One row per shipment carton." - "One row per candidate per stage transition in the hiring pipeline." It is the first thing decided after choosing which business process to model, and it is decided before a single column is chosen. That ordering is the whole discipline: grain is not a summary of the table you built, it is the contract the table is built to satisfy. ## Say it in business language, not as a key list "The grain is date, store, product" is a key column list, not a grain declaration. It cannot be confirmed by a domain expert, and it silently begs the question of whether two rows for the same date, store and product are legitimate. "One row per product per store per day, summarising every ticket that day" *can* be confirmed or rejected by someone who knows the business — and it makes an immediate, testable claim: those three columns are unique. The business sentence is the declaration; the column list is its implementation. ## Grain fixes what else is allowed Once the grain is stated, two design questions stop being matters of taste: **Dimension test.** A dimension attaches to the fact table only if it takes exactly one value for a single row at that grain. At order-line grain, the product qualifies; at daily-store-total grain it does not, because one row spans many products. If a descriptor can take several values for one row, it does not attach directly at that grain and needs another treatment. **Measure test.** A measure belongs on the row only if it is genuinely measured at that grain. An order-level shipping charge is not a line-level measurement; a month-end account balance is not a per-transaction measurement. Forcing a coarser measure onto finer rows is how tables come to double-count. ## Atomic grain is the default Unless there is a specific reason otherwise, the grain is the lowest level the process actually captures. Two properties follow. First, dimensionality is richest at the atomic level: the finer the row, the more descriptors take a single value for it, and every one of them is a way to slice the data. Second, atomic detail is the only level from which every coarser summary can be derived; the reverse is impossible. A model built at atomic grain absorbs questions no one has asked yet, which is the usual reason a warehouse survives its first reorganisation. ## What a declared grain buys you at query time When every row means the same thing, `SUM` over the measures is meaningful without qualification, `COUNT(*)` counts business events rather than storage rows, and a duplicate is definable — a grain violation is precisely two rows where the declaration allows one. When rows mean different things, none of those statements holds, and every query needs undocumented tribal knowledge about which rows to exclude. ## Verify the declaration against the data A declaration nobody checks becomes a comment. The direct test is a uniqueness query on the columns the grain implies: ```sql SELECT order_id, line_number, COUNT(*) FROM fact_order_line GROUP BY order_id, line_number HAVING COUNT(*) > 1; ``` Any rows returned mean either the loader is producing duplicates or the declared grain is wrong — for instance the source splits a line across shipments, so the true grain is one row per line per shipment. Both are worth knowing before a report is built on it, and running this as a standing check keeps a correct declaration correct as sources change. ## Symptoms of an undeclared or violated grain - Two analysts write the same revenue query and get different numbers, and both are defensible. - Reports carry filters like `WHERE row_type <> 'HEADER'` that nobody can explain. - A total is right at one level of a drill-down and wrong at another. - A source change silently doubles a metric, because nothing tested how many rows an event was allowed to produce. Each of these is the same defect surfacing differently: the table's rows do not all mean one thing. ## When the grain has to change Sometimes the business genuinely changes — a shipment can now be split, a subscription can now be paused mid-period — and the old declaration no longer describes reality. The honest response is a new declaration and a rebuild at the finer grain, not a patch that lets both shapes coexist in one table. Mixed shapes in a single fact table are permanently more expensive than one migration, because every future query must carry the exclusion logic. ## What an interviewer listens for They want a grain sentence, unprompted, phrased in business language, plus the two tests it enables. The strongest answers add the verification query — knowing that grain is something you *assert and enforce*, not something you hope stayed true, is what separates a candidate who has read Kimball from one who has operated a warehouse.
- What is the difference between stating the grain as a business sentence and stating it as a list of key columns?A column list describes the implementation and cannot be validated by a domain expert; a sentence like 'one row per product per store per day' can be confirmed or rejected by someone who knows the business, and it makes a testable uniqueness claim. Write the sentence first, then derive the columns from it and enforce them.
- How would you detect that a fact table's real grain differs from its declared grain?Group by the columns the declaration implies and look for counts above one. Rows returned mean either the loader duplicates events or the declaration is wrong — for example the source splits an order line across shipments, making the true grain one row per line per shipment. Run it as a standing check, not once at build time.
- Why does the atomic grain make a model more resilient to new questions?Descriptors that take a single value for a row are richest at the finest level, so atomic rows can be sliced more ways, and every coarser summary can be derived from them while the reverse never works. A model at atomic grain answers questions nobody asked when it was designed.
saying these in an interview costs you the question
- States the grain only as a list of key columns
- Declares the grain after choosing dimensions and measures
- Cannot say what a duplicate row would mean
- Assumes the declared grain still holds without checking
- Lets header-level and line-level rows share one fact table