Skip to main content

Use case

Average order value (AOV) — sometimes called basket size — is revenue divided by the number of orders. It looks like a one-line calculation, but where the two parts live decides how it is modeled:
  • Same cube — both parts are measures of one fact table.
  • Two fact tables — revenue is aggregated at one grain (say, day/item/location) and orders are counted at another (transaction lines). This is the common shape in retail models.
In both cases AOV is a ratio of two aggregates, so it must be computed after its parts are aggregated — never as a row-level amount / orders expression.

Same cube

When both parts are measures of the same cube, define AOV as a calculated measure that divides them:
NULLIF guards the division so a group with no orders returns NULL rather than failing.

Across two fact tables

Retail models usually split the two parts. Sales dollars come from a pre-aggregated daily table (item_location_sales, one row per day, item and location), while the transaction count comes from the line-item table (sales_line_item, one row per transaction line). The two never join to each other — they meet through shared items, locations and dates cubes, which makes this a multi-fact query.
Multi-fact views and multi-stage measures are powered by Tesseract, the next-generation data modeling engine. In versions before v1.7.0, it was not enabled by default.

1. Define each part on the cube that owns it

The denominator counts distinct transactions and excludes exchanges and non-store channels. Write that logic once, as measure filters on the line-item cube, so every consumer picks it up by including the measure — never restate it per view:
Both facts join to the same items, locations and dates cubes. The dates spine matters: without it the two facts have no common time member to group by, since one is keyed by day and the other by timestamp.

2. Define AOV on the view

Neither cube can define AOV — neither can reference the other’s measures. Define it as a measure of the view and mark it multi_stage:
The shared dimension cubes sit at root-level join paths, so date, department and region are common to both facts and can be grouped by.

3. Query it

Querying aov_basket by region aggregates each fact on its own, stitches the two results on the shared dimension, and takes the division over the joined rows:
The measure filters travel into the line-item subquery, so the exchange and channel rules are applied exactly where they were defined.
multi_stage: true is what defers the division until both facts have been aggregated. Without it, Cube plans the expression as an ordinary calculated measure, looks for a single join tree covering both fact cubes, and fails with Can't find join path to join 'locations', 'item_location_sales', 'sales_line_item'.