Chapter 10 · Transaction, Periodic Snapshot, and Accumulating Snapshot Fact Tables
Choosing Multiple Fact Tables for One Business Process Instead of Forcing One Universal Grain
Compare transaction, periodic snapshot, and accumulating snapshot grains side by side, then reject a universal mixed-grain fact through concrete query failures.
Learning outcomes
AtlasMart now has three legitimate analytical needs: atomic sales events, end-of-day inventory state, and order-fulfillment lifecycle progress. A common anti-pattern is to force them into one “fact_activity” table because they share dates, products, or orders. Chapter 10 instead keeps each measurement process at its own grain and aligns answers only after each fact has been aggregated safely.
Compare transaction, periodic snapshot, and accumulating snapshot grains without ranking one as universally superior.
Identify null-heavy and double-counting symptoms of a mixed-grain universal fact.
Map each business question to the fact table whose row meaning matches it.
Drill across separate facts only after grain-appropriate aggregation.
Preserve conformed dimensions without forcing identical foreign-key sets on unrelated grains.
Chapter 10 starts from the accepted Chapter 09 state. Canonical paid sales remain seven order-line facts, four paid orders, nine units, 625 paid GMV, 380 cost-at-sale, and 245 gross profit. Customer SCD history keeps Chapter 07's half-open business-effective intervals; Chapter 08's special-dimension patterns and Chapter 09's bridge/allocation contracts remain valid and are not rewritten here. The September 20 inventory control remains 137 units. Chapter 10 adds a second explicit inventory snapshot for September 19 (146 units) and an order-level fulfillment lifecycle fixture so learners can compare transaction, periodic snapshot, and accumulating snapshot grains without forcing them into one table.
The mandatory lab is synthetic, local, and disposable. It uses
Python's standard-library sqlite3 module, so
learners should record python --version and
sqlite3.sqlite_version when executing. The
fixture proves grain, aggregation, idempotency, and
lifecycle-update semantics in one local process; it does not
claim warehouse-scale performance, distributed exactly-once
delivery, cloud concurrency behavior, or a universal SQL
MERGE contract.
Shared dimensions do not imply shared fact grain. Conformance enables compatible slicing; it does not justify storing every measurement process in the same table.
1. Three facts, three row meanings
| Fact shape | Declared grain | Row behavior | Best questions | Unsafe shortcut |
|---|---|---|---|---|
| Transaction | one paid order line event | insert for each event; corrections follow an explicit policy | what sold, how much, which product/customer/date | derive point-in-time inventory from current sales only |
| Periodic snapshot | one product at one snapshot date | new row for every expected period/member | what was on hand at a date; trend of end-of-day state | sum balances across dates as though they were independent units |
| Accumulating snapshot | one order fulfillment lifecycle | same row revisited as predictable milestones occur | where is the order now; milestone lags; pipeline state | treat each update as a new transaction or expect full event history from the current row |
The same enterprise can legitimately maintain all three. The design question is whether each row has one clear measurement meaning, not whether the warehouse has “too many tables.”
2. Deliberately wrong universal table
CREATE TABLE fact_everything ( row_type TEXT NOT NULL, order_id TEXT, line_no INTEGER, snapshot_date TEXT, product_id TEXT, quantity INTEGER, revenue NUMERIC, on_hand_units INTEGER, shipped_ts TEXT, delivered_ts TEXT);-- Sales rows need order/line/revenue but not snapshot balance.-- Inventory rows need snapshot_date/on_hand but not order/line.-- Lifecycle rows need one row per order and milestone updates.-- NULLs are not the main problem: the row meaning is inconsistent.
Now a query such as COUNT(*) has no stable business
meaning, date columns mix event and snapshot roles, and
consumers can accidentally sum revenue beside repeated inventory
balances. A discriminator does not restore one declared grain.
3. Safe alignment happens after aggregation
Suppose a dashboard wants September 20 paid GMV, end-of-day inventory, and count of orders not yet delivered. Compute each measure from its own fact first, then place the scalar or date-level results side by side. Do not atomic-join order lines to product snapshots: that would repeat the daily balance once per matching sales line.
WITH sales AS ( SELECT substr(event_ts,1,10) AS d, SUM(extended_amount) AS paid_gmv FROM fact_sales_transaction GROUP BY substr(event_ts,1,10)), inventory AS ( SELECT snapshot_date AS d, SUM(on_hand_units) AS on_hand FROM fact_inventory_daily GROUP BY snapshot_date), open_fulfillment AS ( SELECT '2026-09-20' AS d, COUNT(*) AS not_delivered FROM fact_fulfillment_accum WHERE delivered_ts IS NULL OR delivered_ts > '2026-09-20T23:59:59')SELECT s.d,s.paid_gmv,i.on_hand,o.not_deliveredFROM sales sLEFT JOIN inventory i USING(d)LEFT JOIN open_fulfillment o USING(d)WHERE s.d='2026-09-20';
Each CTE reduces its source fact to the requested report grain first. This is the same safety principle used for cross-process drill-across in Chapter 06.
4. Conformed dimensions still matter
Product/date/customer conformance makes independent facts comparable, but every fact includes only dimensions meaningful at its grain. Inventory has no customer dimension. An order-level lifecycle may not carry product when one order contains multiple products; if product-level fulfillment is required, choose a line-level lifecycle grain or a separate bridge/child process rather than stuffing multiple product keys into one order row.
5. Storage and performance tradeoffs
Transaction facts grow with event volume. Dense periodic snapshots grow with periods × covered members. Accumulating snapshots grow with process instances but incur repeated updates. None of those statements implies a universal performance winner. Physical design depends on the target engine, workload, data size, update behavior, partitioning, clustering, compression, concurrency, and retention policy. This chapter evaluates semantic fit first; later chapters address physical/columnar design and performance with measured workloads.
6. Migration and rollback
If AtlasMart previously published a mixed-grain table, migration should run old and new outputs in parallel, map each legacy metric to its new authoritative fact, reconcile historical periods, update semantic/report dependencies, and define rollback criteria. Deleting the old table before consumers prove semantic equivalence converts a modeling improvement into an operational incident.
Knowledge check
Check your understanding
- Why is a discriminator column insufficient to fix a mixed-grain fact?
- When should separate facts be aligned?
- Does a conformed product dimension require inventory and sales to share the same fact table?
- Why might an order-level accumulating fact omit product?
- Which fact type is universally fastest?
Review the answers
1. The rows still represent different measurement processes, so shared aggregates and dimensions have inconsistent meanings.
2. After each fact has been aggregated safely to a compatible reporting grain.
3. No. Conformance enables compatible slicing while preserving independent fact grains.
4. One order can contain multiple products; adding one product key would misrepresent the order-level grain.
5. None. Physical performance is engine- and workload-dependent and must be measured after semantic correctness is established.
Summary and next step
Multiple fact tables are a semantic strength when they preserve distinct measurement processes. Lesson 5 assembles the three shapes into one reproducible acceptance suite and a question-to-fact decision matrix.
Authoritative references
- Kimball Group — Transaction Fact Tables — Primary reference for atomic measurement-event fact tables and transaction grain.
- Kimball Group — Periodic Snapshot Fact Tables — Primary reference for standard-period snapshot grain.
- Kimball Group — Accumulating Snapshot Fact Tables — Primary reference for pipeline/workflow facts whose rows are revisited as milestones occur.
- Kimball Group — Lag/Duration Facts — Reference for milestone lag and duration measures in accumulating snapshots.
- Kimball Group — Fact Tables — Overview of transaction, periodic snapshot, and accumulating snapshot grains.
- SQLite — CREATE TABLE — Constraint semantics used by the deterministic local fixture.
- SQLite — UPSERT — Official SQLite upsert syntax reference; the lab uses explicit event-log idempotency rather than assuming engine-independent merge semantics.
- SQLite — Date And Time Functions — Reference for local deterministic timestamp and duration calculations.