Skip to content

Step Dimensions ​

Sequential processes - web sessions, application funnels, support case escalations - are recorded as one fact row per step. The fact table knows each step happened, but nothing in the row says where it sits in the overall sequence.

A step dimension supplies that context.

Fact Table:

event_idsession_idstep_keypage_nameduration_seconds
E-01S-100STEP-1-3Home10
E-02S-100STEP-2-3Pricing45
E-03S-100STEP-3-3Checkout90

Step Dimension:

step_keystep_numbertotal_stepssteps_remainingis_final_step
STEP-1-3132No
STEP-2-3231No
STEP-3-3330Yes
STEP-1-7176No
STEP-2-7275No

The last two rows belong to a session of a different length. STEP-1-3 and STEP-1-7 are both "step 1", but one has two steps remaining and the other six.

A row exists for every (step_number, total_steps) pair, so one dimension serves sessions of any length. It stays small - a few hundred rows covers every realistic session - and is reused across every fact row.

Position then becomes an ordinary attribute filter: abandoned sessions are total_steps = 1, and the average duration of a final step is a filter on is_final_step. Without the dimension, each needs a window function or a self-join.

Why not just put the step columns on the fact table?

You certainly can. The separate dimension was originally designed for older row-based databases, where extra columns added storage costs to every fact row. Modern columnar databases largely eliminate this problem.

However, a step dimension still makes it much easier to add new attributes later. For example, adding a new category like funnel_phase is a simple update to a 200-row dimension table, rather than a massive rewrite of billions of fact rows.

As a middle ground, you can store just the step_number and total_steps directly on the fact table and introduce a step dimension later, joining on (step_number, total_steps). That pair is a valid natural key as long as every dimension attribute is determined by those two values.

The step key can't be assigned until the session ends

total_steps is unknown while a session is in progress, so the key for its first event depends on how many events follow. Sessions must therefore be loaded as complete units, and "ended" needs a definition - usually an inactivity timeout. For streaming loads, either stage events until the session closes or assign a placeholder member and update the keys afterwards.