Chapter 10 · Transaction, Periodic Snapshot, and Accumulating Snapshot Fact Tables

Accumulating Snapshots for Pipeline/Workflow Milestones, Reopened Processes, and Updating Rows

Build an order-level accumulating fulfillment snapshot, apply milestone events idempotently, handle a reopened process, and explain what update-in-place preserves and loses.

Intermediate → Advanced120–140 minutesAccumulating lifecycle labPython stdlib sqlite3 · local/syntheticLast reviewed: September 2026

Learning outcomes

AtlasMart now wants one operationally useful row that answers “where is order O1002 in fulfillment, how long did it take to reach shipment, and which orders are still open?” Reconstructing that answer from many independent events is possible, but a predictable workflow can also be represented by an accumulating snapshot whose row is intentionally revisited as milestones occur.

01

Declare an accumulating snapshot grain as one order fulfillment lifecycle.

02

Use separate milestone timestamps rather than overloading one generic status date.

03

Apply events idempotently so replay does not advance the row twice.

04

Handle a reopened workflow without pretending the accumulating row is a complete event history.

05

Derive milestone lags while preserving the raw milestone timestamps that support them.

Chapter 10 continuity contract

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.

Execution and interpretation note

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.

Important boundary

An accumulating snapshot is intentionally mutable. That does not make arbitrary updates safe: milestone ordering, event identity, replay behavior, correction policy, and reopened-process semantics must be explicit.

1. One row per fulfillment lifecycle

AtlasMart chooses one row per order fulfillment lifecycle for the teaching fixture. The row starts when the order is observed and gains paid_ts, picked_ts, shipped_ts, and delivered_ts as those predictable steps occur. current_status is a convenience state, not a replacement for the milestone fields.

Order Expected current state Notable history
O1001 reopened ordered → paid → picked → shipped → delivered → reopened
O1002 shipped ordered → paid → picked → shipped
O1003 delivered ordered → paid → picked → shipped → delivered
O1005 picked ordered → paid → picked

2. Event-log gate plus accumulating row

accumulating_snapshot_schema.sql
CREATE TABLE fact_fulfillment_accum (  order_id TEXT PRIMARY KEY,  order_ts TEXT, paid_ts TEXT, picked_ts TEXT, shipped_ts TEXT, delivered_ts TEXT,  reopened_ts TEXT,  current_status TEXT NOT NULL,  reopen_count INTEGER NOT NULL DEFAULT 0,  last_event_seq INTEGER NOT NULL DEFAULT 0,  updated_at TEXT NOT NULL);CREATE TABLE fulfillment_event_log (  event_id TEXT PRIMARY KEY,  order_id TEXT NOT NULL,  event_seq INTEGER NOT NULL,  event_type TEXT NOT NULL,  event_ts TEXT NOT NULL,  ingested_at TEXT NOT NULL,  UNIQUE(order_id,event_seq));

The event log provides deduplication and audit evidence for this lab. The accumulating row is the current workflow summary. Keeping both makes the semantics explicit: one table preserves accepted source events; the other is updated for convenient lifecycle analysis.

3. Idempotent application logic

apply_event.py
def apply_event(conn, event):    event_id, order_id, seq, event_type, event_ts, ingested_at = event    try:        conn.execute(            'INSERT INTO fulfillment_event_log VALUES (?,?,?,?,?,?)', event        )    except sqlite3.IntegrityError:        return False  # replayed event: summary row must not change    conn.execute(        """INSERT OR IGNORE INTO fact_fulfillment_accum           (order_id,current_status,reopen_count,last_event_seq,updated_at)           VALUES (?,?,0,0,?)""",        (order_id, 'new', ingested_at),    )    # Then update only the named milestone for a new higher event sequence.    # The complete Chapter 10 acceptance script performs this deterministically.    return True

A duplicate event ID returns False and cannot increment reopen_count twice. This is local loader idempotency; it is not a claim of distributed exactly-once processing.

4. Deliberately wrong approach: treat the row as immutable

If AtlasMart inserts one lifecycle row at order creation and refuses to update it, every later milestone would require another row—silently converting the design into a transaction/event fact with a different grain. Conversely, overwriting one generic status_ts loses the earlier milestones. The accumulating pattern works because the same process row is updated while separate milestone columns preserve the predictable steps.

5. Reopened processes expose the limits of the summary

O1001 is delivered and then reopened on September 21. The lab preserves the original delivered timestamp, records reopened_ts, increments reopen_count, and changes the current state to reopened. If the process can reopen many times or revisit arbitrary steps, a single accumulating row cannot preserve every transition; keep an event fact/log or another history structure alongside it.

reopened_process_query.sql
SELECT order_id,delivered_ts,reopened_ts,reopen_count,current_statusFROM fact_fulfillment_accumWHERE reopen_count > 0;-- O1001 | 2026-09-20T11:00:00 | 2026-09-21T12:00:00 | 1 | reopened

6. Lag measurements

Milestone timestamps support durations such as order-to-ship or ship-to-deliver. Store a derived lag only when its business calendar and rounding policy are governed; otherwise compute it from the authoritative milestones.

milestone_lag.sql
SELECT order_id,       ROUND((julianday(shipped_ts)-julianday(order_ts))*24.0,2) AS order_to_ship_hoursFROM fact_fulfillment_accumWHERE shipped_ts IS NOT NULLORDER BY order_id;

SQLite’s date arithmetic is only the local execution harness. A production business-hours SLA may require calendars, time zones, holiday exclusions, and service-specific rules that simple elapsed hours do not capture.

7. Production judgment

Accumulating snapshots are valuable when process milestones are predictable and current pipeline state plus lag analysis matter. They are a poor sole record for arbitrary event histories. Keep event identity, ordering, correction/reopen rules, and downstream restatement behavior visible; if a late event arrives with an older sequence or business time, route it through a defined correction path instead of silently moving the lifecycle backwards.

Knowledge check

Check your understanding

  1. Why is an accumulating snapshot allowed to update?
  2. What prevents a replayed reopen event from incrementing twice in the lab?
  3. Why keep milestone columns instead of one status timestamp?
  4. What does a reopened process reveal?
  5. When is a stored lag risky?
Review the answers

1. Its grain is one workflow lifecycle, and the row summarizes predictable milestones as that same process progresses.

2. The unique event ID/event sequence gate in fulfillment_event_log.

3. Separate milestones preserve the key lifecycle dates needed for lag analysis and audit.

4. A single accumulating row summarizes current lifecycle state but may not preserve every repeated transition; event history is still needed when that detail matters.

5. When business-calendar, time-zone, or rounding rules are not governed or may change.

Summary and next step

The accumulating snapshot intentionally revisits one process row while preserving milestone evidence. Lesson 4 compares all three fact shapes and demonstrates why combining their grains into one universal table is a modeling error.

Authoritative references

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.