diff --git a/docs/pages/product/caching/recipes/non-additivity.mdx b/docs/pages/product/caching/recipes/non-additivity.mdx
index bd959dc3aea77..e7cf33d942c05 100644
--- a/docs/pages/product/caching/recipes/non-additivity.mdx
+++ b/docs/pages/product/caching/recipes/non-additivity.mdx
@@ -165,8 +165,7 @@ cube(`users`, {
### Decomposing into a formula with additive measures
-Non-additive `avg` measures can be rewritten as
-[calculated measures](/product/data-modeling/reference/measures#calculated-measures)
+Non-additive `avg` measures can be rewritten as [calculated measures][ref-calculated-measures]
that reference additive measures only. Then, this additive measures can be used
in pre-aggregations. Please note, however, that you shouldn't include `avg_age`
measure in your pre-aggregation as it renders it non-additive.
@@ -245,4 +244,5 @@ or run it with the `docker-compose up` command. You'll see the result, including
queried data, in the console.
-[ref-percentile-recipe]: /product/data-modeling/recipes/percentiles
\ No newline at end of file
+[ref-percentile-recipe]: /product/data-modeling/recipes/percentiles
+[ref-calculated-measures]: /product/data-modeling/concepts/calculated-members#calculated-measures
\ No newline at end of file
diff --git a/docs/pages/product/data-modeling/concepts.mdx b/docs/pages/product/data-modeling/concepts.mdx
index d175a3d0562a4..588406a7ce6e6 100644
--- a/docs/pages/product/data-modeling/concepts.mdx
+++ b/docs/pages/product/data-modeling/concepts.mdx
@@ -42,7 +42,7 @@ metrics-first approaches.
_Cubes_ represent datasets in Cube and are conceptually similar to [views in
SQL][wiki-view-sql]. Cubes are usually declared in separate files with one
cube per file. Typically, a cube points to a single table in
-your database using the [`sql_table` property][ref-schema-ref-sql-table]:
+your [data source][ref-data-sources] using the [`sql_table` property][ref-schema-ref-sql-table]:
@@ -104,6 +104,9 @@ Cubes and their members can be further referenced by [views](#views).
Note that cubes support [extension][ref-extending-cubes],
[polymorphism][ref-polymorphic-cubes], and [data blending][ref-data-blending].
+Custom calendars, such as retail calendars, can be implemented using [calendar
+cubes][ref-calendar-cubes].
+
Cubes can be defined statically and you can also build [dynamic data
models][ref-dynamic-data-models].
@@ -850,4 +853,6 @@ See the reference documentaton for the full list of pre-aggregation
[ref-custom-calendar-recipe]: /product/data-modeling/recipes/custom-calendar
[ref-cube-with-dbt]: /product/data-modeling/recipes/dbt
[ref-apis-support]: /product/apis-integrations#data-modeling
-[ref-viz-tools]: /product/configuration/visualization-tools
\ No newline at end of file
+[ref-viz-tools]: /product/configuration/visualization-tools
+[ref-data-sources]: /product/configuration/data-sources
+[ref-calendar-cubes]: /product/data-modeling/concepts/calendar-cubes
\ No newline at end of file
diff --git a/docs/pages/product/data-modeling/concepts/_meta.js b/docs/pages/product/data-modeling/concepts/_meta.js
index 5c26138e38f43..0c799f03ac780 100644
--- a/docs/pages/product/data-modeling/concepts/_meta.js
+++ b/docs/pages/product/data-modeling/concepts/_meta.js
@@ -2,7 +2,8 @@ module.exports = {
"calculated-members": "Calculated members",
"multi-stage-calculations": "Multi-stage calculations",
"working-with-joins": "Joins between cubes",
+ "calendar-cubes": "Calendar cubes",
"code-reusability-extending-cubes": "Extension",
"polymorphic-cubes": "Polymorphic cubes",
"data-blending": "Data blending"
-}
\ No newline at end of file
+}
diff --git a/docs/pages/product/data-modeling/concepts/calculated-members.mdx b/docs/pages/product/data-modeling/concepts/calculated-members.mdx
index e5faefb5b8c4e..053efa0a957c6 100644
--- a/docs/pages/product/data-modeling/concepts/calculated-members.mdx
+++ b/docs/pages/product/data-modeling/concepts/calculated-members.mdx
@@ -17,6 +17,8 @@ into formulas that involve simpler measures. Also, calculated measures [can
help][ref-decomposition-recipe] to use [non-additive][ref-non-additive] measures with
pre-aggregations.
+### Members of the same cube
+
In the following example, the `completed_ratio` measure is calculated as a division of
`completed_count` by total `count`. Note that the result is also multiplied by `1.0`
since [integer division in SQL][link-postgres-division] would otherwise produce an
@@ -90,6 +92,149 @@ FROM (
) AS "orders"
```
+### Members of other cubes
+
+If you have `first_cube` that is [joined][ref-joins] to `second_cube`, you can define a
+calculated measure that references measures from both `first_cube` and `second_cube`.
+When you query for this calculated measure, Cube will transparently generate SQL with
+necessary joins.
+
+In the following example, the `orders.purchases_to_users_ratio` measure references the
+`purchases` measure from the `orders` cube and the `count` measure from the `users` cube:
+
+
+
+```javascript
+cube(`orders`, {
+ sql: `
+ SELECT 1 AS id, 11 AS user_id, 'processing' AS status UNION ALL
+ SELECT 2 AS id, 11 AS user_id, 'completed' AS status UNION ALL
+ SELECT 3 AS id, 11 AS user_id, 'completed' AS status
+ `,
+
+ dimensions: {
+ id: {
+ sql: `id`,
+ type: `number`,
+ primary_key: true
+ }
+ },
+
+ measures: {
+ purchases: {
+ type: `count`,
+ filters: [{
+ sql: `${CUBE}.status = 'completed'`
+ }]
+ }
+ }
+})
+
+cube(`users`, {
+ sql: `
+ SELECT 11 AS id, 'Alice' AS name UNION ALL
+ SELECT 12 AS id, 'Bob' AS name UNION ALL
+ SELECT 13 AS id, 'Eve' AS name
+ `,
+
+ joins: {
+ orders: {
+ sql: `${CUBE}.id = ${orders}.user_id`,
+ relationship: `one_to_many`
+ }
+ },
+
+ dimensions: {
+ id: {
+ sql: `id`,
+ type: `number`,
+ primary_key: true
+ }
+ },
+
+ measures: {
+ count: {
+ type: `count`
+ },
+
+ purchases_to_users_ratio: {
+ sql: `100.0 * ${orders.purchases} / ${CUBE.count}`,
+ type: `number`,
+ format: `percent`
+ }
+ }
+})
+```
+
+```yaml
+cubes:
+ - name: orders
+ sql: >
+ SELECT 1 AS id, 11 AS user_id, 'processing' AS status UNION ALL
+ SELECT 2 AS id, 11 AS user_id, 'completed' AS status UNION ALL
+ SELECT 3 AS id, 11 AS user_id, 'completed' AS status
+
+ dimensions:
+ - name: id
+ sql: id
+ type: number
+ primary_key: true
+
+ measures:
+ - name: purchases
+ type: count
+ filters:
+ - sql: "{CUBE}.status = 'completed'"
+
+ - name: users
+ sql: >
+ SELECT 11 AS id, 'Alice' AS name UNION ALL
+ SELECT 12 AS id, 'Bob' AS name UNION ALL
+ SELECT 13 AS id, 'Eve' AS name
+
+ joins:
+ - name: orders
+ sql: "{CUBE}.id = {orders}.user_id"
+ relationship: one_to_many
+
+ dimensions:
+ - name: id
+ sql: id
+ type: number
+ primary_key: true
+
+ measures:
+ - name: count
+ type: count
+
+ - name: purchases_to_users_ratio
+ sql: "1.0 * {orders.purchases} / {CUBE.count}"
+ type: number
+```
+
+
+
+If you query for `users.purchases_to_users_ratio`, Cube will generate the following SQL:
+
+```sql
+SELECT
+ 1.0 * COUNT(
+ CASE
+ WHEN ("orders".status = 'completed') THEN "orders".id
+ END
+ ) / COUNT(DISTINCT "users".id) "users__purchases_to_users_ratio"
+FROM (
+ SELECT 11 AS id, 'Alice' AS name UNION ALL
+ SELECT 12 AS id, 'Bob' AS name UNION ALL
+ SELECT 13 AS id, 'Eve' AS name
+) AS "users"
+LEFT JOIN (
+ SELECT 1 AS id, 11 AS user_id, 'processing' AS status UNION ALL
+ SELECT 2 AS id, 11 AS user_id, 'completed' AS status UNION ALL
+ SELECT 3 AS id, 11 AS user_id, 'completed' AS status
+) AS "orders" ON "users".id = "orders".user_id
+```
+
## Proxy dimensions
**Proxy dimensions reference dimensions from the same cube or other cubes.**
diff --git a/docs/pages/product/data-modeling/concepts/calendar-cubes.mdx b/docs/pages/product/data-modeling/concepts/calendar-cubes.mdx
new file mode 100644
index 0000000000000..fe18dc10f6e24
--- /dev/null
+++ b/docs/pages/product/data-modeling/concepts/calendar-cubes.mdx
@@ -0,0 +1,459 @@
+# Calendar cubes
+
+_Calendar cubes_ are used to implement custom calendars, such as retail calendars.
+If your data model contains a calendar table, it can be modeled as a calendar cube.
+
+Calendar cubes can be used to [override](#overriding-time-shifts) the default time
+shift behavior of time-shift measures as well as [override](#overriding-granularities)
+the default granularities of time dimensions.
+
+## Configuration
+
+Calendar cubes are [cubes][ref-cubes] where the [`calendar` parameter][ref-cubes-calendar]
+is set to `true`. This indicates that the cube is a calendar cube and allow the use of
+custom time shifts and granularities.
+
+
+
+```yaml
+cubes:
+ - name: fiscal_calendar
+ calendar: true
+ sql: >
+ SELECT
+ date_key,
+ calendar_date,
+ start_of_week, start_of_month, start_of_year,
+ week_ago, month_ago, year_ago
+ FROM calendar_table
+
+ dimensions:
+ - name: date_key
+ sql: date
+ type: time
+ primary_key: true
+
+ - name: date
+ sql: date
+ type: time
+
+ time_shift:
+ - type: prior
+ interval: 1 week
+ sql: "{CUBE}.week_ago"
+
+ - type: prior
+ interval: 1 month
+ sql: "{CUBE}.month_ago"
+
+ - type: prior
+ interval: 1 year
+ sql: "{CUBE}.year_ago"
+
+ granularities:
+ - name: week
+ sql: "{CUBE}.start_of_week"
+
+ - name: month
+ sql: "{CUBE}.start_of_month"
+
+ - name: year
+ sql: "{CUBE}.start_of_year"
+```
+
+```javascript
+cube('fiscal_calendar', {
+ calendar: true,
+ sql: `
+ SELECT
+ date_key,
+ calendar_date,
+ start_of_week, start_of_month, start_of_year,
+ week_ago, month_ago, year_ago
+ FROM calendar_table
+ `,
+
+ dimensions: {
+ date_key: {
+ sql: 'date_key',
+ type: 'time',
+ primary_key: true
+ },
+
+ date: {
+ sql: 'calendar_date',
+ type: 'time',
+
+ time_shift: [
+ { type: 'prior', interval: '1 week', sql: '{CUBE}.week_ago' },
+ { type: 'prior', interval: '1 month', sql: '{CUBE}.month_ago' },
+ { type: 'prior', interval: '1 year', sql: '{CUBE}.year_ago' }
+ ],
+
+ granularities: [
+ { name: 'week', sql: '{CUBE}.start_of_week' },
+ { name: 'month', sql: '{CUBE}.start_of_month' },
+ { name: 'year', sql: '{CUBE}.start_of_year' }
+ ]
+ }
+ }
+})
+```
+
+
+
+Calendar cubes are only useful when they are joined with other cubes in the data model.
+
+
+
+```yaml
+cubes:
+ - name: sales
+ sql_table: sales_facts
+
+ joins:
+ - name: fiscal_calendar
+ sql: "{CUBE}.date = {fiscal_calendar.date_key}"
+ relationship: many_to_one
+
+ # ...
+```
+
+```javascript
+cube(`sales`, {
+ sql_table: `sales_facts`,
+
+ joins: {
+ fiscal_calendar: {
+ sql: `${CUBE}.date = ${fiscal_calendar.date_key}`,
+ relationship: `many_to_one`
+ }
+ },
+
+ // ...
+})
+```
+
+
+
+## Overriding time shifts
+
+Calendar cubes can be used to override the default time shift behavior of [time-shift
+measures][ref-time-shift]. It can help implement custom time shifts or reuse common time
+shifts across multiple cubes.
+
+By default, a time shift like `prior` + `1 month` will add `INTERVAL '1 month'` to the
+time dimension value in the generated SQL. However, with custom calendars, a more nuanced
+approach is often needed, such as mapping each date to another pre-calculated date from
+the calendar table.
+
+In the following example, the `custom_calendar` cube defines a custom time shift for
+`prior` + `1 month` that uses the `month_ago` column from the calendar table. It also
+defines a custom time shift `my_favorite_time_shift` of type `prior` + the `42 days`
+interval.
+
+
+
+```yaml
+cubes:
+ - name: custom_calendar
+ calendar: true
+ sql: >
+ SELECT '2025-01-01' AS date, '2024-12-15' AS month_ago UNION ALL
+ SELECT '2025-02-01' AS date, '2025-01-15' AS month_ago UNION ALL
+ SELECT '2025-03-01' AS date, '2025-02-15' AS month_ago UNION ALL
+ SELECT '2025-04-01' AS date, '2025-03-15' AS month_ago UNION ALL
+ SELECT '2025-05-01' AS date, '2025-04-15' AS month_ago UNION ALL
+ SELECT '2025-06-01' AS date, '2025-05-15' AS month_ago
+
+ dimensions:
+ - name: date_key
+ sql: "{CUBE}.date::TIMESTAMP"
+ type: time
+ primary_key: true
+
+ - name: date
+ sql: "{CUBE}.date::TIMESTAMP"
+ type: time
+
+ time_shift:
+ - type: prior
+ interval: 1 month
+ sql: "{CUBE}.month_ago::TIMESTAMP"
+
+ - type: prior
+ interval: 42 days
+ name: my_favorite_time_shift
+
+ - name: sales
+ sql: >
+ SELECT 1 AS id, 101 AS amount, '2025-01-01'::TIMESTAMP AS date UNION ALL
+ SELECT 2 AS id, 202 AS amount, '2025-02-01'::TIMESTAMP AS date UNION ALL
+ SELECT 3 AS id, 303 AS amount, '2025-03-01'::TIMESTAMP AS date UNION ALL
+ SELECT 4 AS id, 404 AS amount, '2025-04-01'::TIMESTAMP AS date UNION ALL
+ SELECT 5 AS id, 505 AS amount, '2025-05-01'::TIMESTAMP AS date UNION ALL
+ SELECT 6 AS id, 606 AS amount, '2025-06-01'::TIMESTAMP AS date
+
+ joins:
+ - name: custom_calendar
+ sql: "{CUBE}.date = {custom_calendar.date_key}"
+ relationship: many_to_one
+
+ dimensions:
+ - name: id
+ sql: id
+ type: number
+ primary_key: true
+
+ measures:
+ - name: total_sales
+ sql: amount
+ type: sum
+
+ - name: total_sales_prior_month
+ sql: "{total_sales}"
+ type: number
+ time_shift:
+ - type: prior
+ interval: 1 month
+
+ - name: total_sales_few_days_ago
+ sql: "{total_sales}"
+ type: number
+ time_shift:
+ - name: my_favorite_time_shift
+```
+
+```javascript
+cube(`custom_calendar`, {
+ calendar: true,
+ sql: `
+ SELECT '2025-01-01' AS date, '2024-12-15' AS month_ago UNION ALL
+ SELECT '2025-02-01' AS date, '2025-01-15' AS month_ago UNION ALL
+ SELECT '2025-03-01' AS date, '2025-02-15' AS month_ago UNION ALL
+ SELECT '2025-04-01' AS date, '2025-03-15' AS month_ago UNION ALL
+ SELECT '2025-05-01' AS date, '2025-04-15' AS month_ago UNION ALL
+ SELECT '2025-06-01' AS date, '2025-05-15' AS month_ago
+ `,
+
+ dimensions: {
+ date_key: {
+ sql: `${CUBE}.date::TIMESTAMP`,
+ type: `time`,
+ primary_key: true
+ },
+
+ date: {
+ sql: `${CUBE}.date::TIMESTAMP`,
+ type: `time`,
+
+ time_shift: [
+ { type: `prior`, interval: `1 month`, sql: `${CUBE}.month_ago::TIMESTAMP` },
+ { type: `prior`, interval: `42 days`, name: `my_favorite_time_shift` }
+ ]
+ }
+ }
+})
+
+cube(`sales`, {
+ sql: `
+ SELECT 1 AS id, 101 AS amount, '2025-01-01'::TIMESTAMP AS date UNION ALL
+ SELECT 2 AS id, 202 AS amount, '2025-02-01'::TIMESTAMP AS date UNION ALL
+ SELECT 3 AS id, 303 AS amount, '2025-03-01'::TIMESTAMP AS date UNION ALL
+ SELECT 4 AS id, 404 AS amount, '2025-04-01'::TIMESTAMP AS date UNION ALL
+ SELECT 5 AS id, 505 AS amount, '2025-05-01'::TIMESTAMP AS date UNION ALL
+ SELECT 6 AS id, 606 AS amount, '2025-06-01'::TIMESTAMP AS date
+ `,
+
+ joins: {
+ custom_calendar: {
+ sql: `${CUBE}.date = ${custom_calendar.date_key}`,
+ relationship: `many_to_one`
+ }
+ },
+
+ dimensions: {
+ id: {
+ sql: `id`,
+ type: `number`,
+ primary_key: true
+ }
+ },
+
+ measures: {
+ total_sales: {
+ sql: `amount`,
+ type: `sum`
+ },
+
+ total_sales_prior_month: {
+ sql: `{total_sales}`,
+ type: `number`,
+ time_shift: [
+ { type: `prior`, interval: `1 month` }
+ ]
+ },
+
+ total_sales_few_days_ago: {
+ sql: `{total_sales}`,
+ type: `number`,
+ time_shift: [
+ { name: `my_favorite_time_shift` }
+ ]
+ }
+ }
+})
+```
+
+
+
+Whe `sales.total_sales_prior_month` and `sales.total_sales_few_days_ago` measures are
+queried together with the `calendar.date` time dimension, the generate SQL will use the
+custom time shifts defined in the `custom_calendar` cube: one with the `month_ago`
+column and another with `INTERVAL '42 days'`.
+
+## Overriding granularities
+
+Calendar cubes can be used to override the default [granularities][ref-granularities] of
+[time dimensions][ref-time-dimension].
+
+By default, SQL functions like `DATE_TRUNC` are used to calculate default granularities,
+such as `day`, `month`, or `year`. However, custom calendars often have different
+definitions for these periods, e.g., a retail calendar might use 4-5-4 week patterns.
+
+Calendar cubes allow you to define custom SQL expressions for each granularity.
+In the following example, the `fiscal_calendar` cube overrides the default `month`
+granularity to the to a pre-calculated `mid_month` column:
+
+
+
+```yaml
+cubes:
+ - name: custom_calendar
+ calendar: true
+ sql: >
+ SELECT '2025-01-02' AS date, '2025-01-15' AS mid_month UNION ALL
+ SELECT '2025-02-04' AS date, '2025-02-15' AS mid_month UNION ALL
+ SELECT '2025-03-09' AS date, '2025-03-15' AS mid_month UNION ALL
+ SELECT '2025-04-17' AS date, '2025-04-15' AS mid_month UNION ALL
+ SELECT '2025-05-21' AS date, '2025-05-15' AS mid_month UNION ALL
+ SELECT '2025-06-30' AS date, '2025-06-15' AS mid_month
+
+ dimensions:
+ - name: date_key
+ sql: date
+ type: time
+ primary_key: true
+
+ - name: date
+ sql: date
+ type: time
+ primary_key: true
+
+ granularities:
+ - name: month
+ sql: "{CUBE}.mid_month::TIMESTAMP"
+
+ - name: sales
+ sql: >
+ SELECT 1 AS id, 101 AS amount, '2025-01-02'::TIMESTAMP AS date UNION ALL
+ SELECT 2 AS id, 202 AS amount, '2025-02-04'::TIMESTAMP AS date UNION ALL
+ SELECT 3 AS id, 303 AS amount, '2025-03-09'::TIMESTAMP AS date UNION ALL
+ SELECT 4 AS id, 404 AS amount, '2025-04-17'::TIMESTAMP AS date UNION ALL
+ SELECT 5 AS id, 505 AS amount, '2025-05-21'::TIMESTAMP AS date UNION ALL
+ SELECT 6 AS id, 606 AS amount, '2025-06-30'::TIMESTAMP AS date
+
+ joins:
+ - name: custom_calendar
+ sql: "{CUBE}.date = {custom_calendar.date}"
+ relationship: many_to_one
+
+ dimensions:
+ - name: id
+ sql: id
+ type: number
+ primary_key: true
+
+ measures:
+ - name: revenue
+ sql: amount
+ type: sum
+```
+
+```javascript
+cube(`custom_calendar`, {
+ calendar: true,
+ sql: `
+ SELECT '2025-01-02' AS date, '2025-01-15' AS mid_month UNION ALL
+ SELECT '2025-02-04' AS date, '2025-02-15' AS mid_month UNION ALL
+ SELECT '2025-03-09' AS date, '2025-03-15' AS mid_month UNION ALL
+ SELECT '2025-04-17' AS date, '2025-04-15' AS mid_month UNION ALL
+ SELECT '2025-05-21' AS date, '2025-05-15' AS mid_month UNION ALL
+ SELECT '2025-06-30' AS date, '2025-06-15' AS mid_month
+ `,
+
+ dimensions: {
+ date_key: {
+ sql: `date`,
+ type: `time`,
+ primary_key: true
+ },
+
+ date: {
+ sql: `date`,
+ type: `time`,
+ primary_key: true,
+
+ granularities: [
+ { name: `month`, sql: `${CUBE}.mid_month::TIMESTAMP` }
+ ]
+ }
+ }
+})
+
+cube(`sales`, {
+ sql: `
+ SELECT 1 AS id, 101 AS amount, '2025-01-02'::TIMESTAMP AS date UNION ALL
+ SELECT 2 AS id, 202 AS amount, '2025-02-04'::TIMESTAMP AS date UNION ALL
+ SELECT 3 AS id, 303 AS amount, '2025-03-09'::TIMESTAMP AS date UNION ALL
+ SELECT 4 AS id, 404 AS amount, '2025-04-17'::TIMESTAMP AS date UNION ALL
+ SELECT 5 AS id, 505 AS amount, '2025-05-21'::TIMESTAMP AS date UNION ALL
+ SELECT 6 AS id, 606 AS amount, '2025-06-30'::TIMESTAMP AS date
+ `,
+
+ joins: {
+ custom_calendar: {
+ sql: `${CUBE}.date = ${custom_calendar.date}`,
+ relationship: `many_to_one`
+ }
+ },
+
+ dimensions: {
+ id: {
+ sql: `id`,
+ type: `number`,
+ primary_key: true
+ }
+ },
+
+ measures: {
+ revenue: {
+ sql: `amount`,
+ type: `sum`
+ }
+ }
+})
+```
+
+
+
+When querying `sales.revenue` by `custom_calendar.date` with monthly granularity, the
+`mid_month` column will be used instead of the standard `DATE_TRUNC('month', date)`
+expression in the generated SQL.
+
+
+[ref-time-shift]: /product/data-modeling/concepts/multi-stage-calculations#time-shift
+[ref-time-dimension]: /product/data-modeling/concepts#time-dimensions
+[ref-granularities]: /product/data-modeling/reference/dimensions#granularities
+[ref-cubes]: /product/data-modeling/reference/cube
+[ref-cubes-calendar]: /product/data-modeling/reference/cube#calendar
\ No newline at end of file
diff --git a/docs/pages/product/data-modeling/concepts/multi-stage-calculations.mdx b/docs/pages/product/data-modeling/concepts/multi-stage-calculations.mdx
index e0e179e046ebc..d2b227764d9ab 100644
--- a/docs/pages/product/data-modeling/concepts/multi-stage-calculations.mdx
+++ b/docs/pages/product/data-modeling/concepts/multi-stage-calculations.mdx
@@ -26,8 +26,8 @@ Please track [this issue](https://github.com/cube-js/cube/issues/8487).
Common uses of multi-stage calculations:
- [Rolling window](#rolling-window) calculations.
+- [Time-shift](#time-shift) calculations, e.g., year-over-year sales growth.
- [Period-to-date](#period-to-date) calculations, e.g., year-to-date (YTD) analysis.
-- [Prior date](#prior-date) calculations, e.g., year-over-year sales growth.
- [Fixed dimension](#fixed-dimension) calculations, e.g., comparing individual items to a broader dataset or calculating percent of total.
- [Ranking](#ranking) calculations.
@@ -98,45 +98,48 @@ Query and result:
-## Period-to-date
+## Time shift
-Period-to-date calculations can be used to analyze data over different time periods:
+A _time-shift measure_ calculates the value of another measure at a different point in
+time. This is achieved by _shifting_ the time dimension from the query in the necessary
+direction during the calculation. Time-shifts are configured using the [`time_shift`
+parameter][ref-ref-time-shift] of a measure.
-- Year-to-date (YTD) analysis.
-- Quarter-to-date (QTD) analysis.
-- Month-to-date (MTD) analysis.
+Typically, this is used to compare the current value of a measure with its prior value,
+such as the same time last year. For example, if you have the `revenue` measure, you can
+calculate its value for the same time last year:
```yaml
-- name: revenue_ytd
- sql: revenue
- type: sum
- rolling_window:
- type: to_date
- granularity: year
-
-- name: revenue_qtd
- sql: revenue
- type: sum
- rolling_window:
- type: to_date
- granularity: quarter
-
-- name: revenue_mtd
- sql: revenue
- type: sum
- rolling_window:
- type: to_date
- granularity: month
+- name: revenue_prior_year
+ multi_stage: true
+ sql: "{revenue}"
+ type: number
+ time_shift:
+ - interval: 1 year
+ type: prior
```
+You can use time-shift measures with [calendar cubes][ref-calendar-cubes] to customize
+how time-shifting works, e.g., to shift the time dimension to the prior date in a retail
+calendar.
+
### Example
Data model:
```yaml
cubes:
- - name: period_to_date
+ - name: prior_date
sql: |
+ SELECT '2023-04-01'::TIMESTAMP AS time, 1000 AS revenue UNION ALL
+ SELECT '2023-05-01'::TIMESTAMP AS time, 1000 AS revenue UNION ALL
+ SELECT '2023-06-01'::TIMESTAMP AS time, 1000 AS revenue UNION ALL
+ SELECT '2023-07-01'::TIMESTAMP AS time, 1000 AS revenue UNION ALL
+ SELECT '2023-08-01'::TIMESTAMP AS time, 1000 AS revenue UNION ALL
+ SELECT '2023-09-01'::TIMESTAMP AS time, 1000 AS revenue UNION ALL
+ SELECT '2023-10-01'::TIMESTAMP AS time, 1000 AS revenue UNION ALL
+ SELECT '2023-11-01'::TIMESTAMP AS time, 1000 AS revenue UNION ALL
+ SELECT '2023-12-01'::TIMESTAMP AS time, 1000 AS revenue UNION ALL
SELECT '2024-01-01'::TIMESTAMP AS time, 1000 AS revenue UNION ALL
SELECT '2024-02-01'::TIMESTAMP AS time, 1000 AS revenue UNION ALL
SELECT '2024-03-01'::TIMESTAMP AS time, 1000 AS revenue UNION ALL
@@ -162,55 +165,71 @@ cubes:
type: time
measures:
- - name: revenue_ytd
- sql: revenue
- type: sum
- rolling_window:
- type: to_date
- granularity: year
-
- - name: revenue_qtd
+ - name: revenue
sql: revenue
type: sum
- rolling_window:
- type: to_date
- granularity: quarter
-
- - name: revenue_mtd
+
+ - name: revenue_ytd
sql: revenue
type: sum
rolling_window:
type: to_date
- granularity: month
+ granularity: year
+
+ - name: revenue_prior_year
+ multi_stage: true
+ sql: "{revenue}"
+ type: number
+ time_shift:
+ - time_dimension: time
+ interval: 1 year
+ type: prior
+
+ - name: revenue_prior_year_ytd
+ multi_stage: true
+ sql: "{revenue_ytd}"
+ type: number
+ time_shift:
+ - time_dimension: time
+ interval: 1 year
+ type: prior
```
-Query and result:
+Queries and results:
-
+
-## Prior date
+
+
+## Period-to-date
-Prior date calculations can be used to find the difference between two aggregated
-measures, like year-over-year sales growth.
+Period-to-date calculations can be used to analyze data over different time periods:
+
+- Year-to-date (YTD) analysis.
+- Quarter-to-date (QTD) analysis.
+- Month-to-date (MTD) analysis.
```yaml
-- name: revenue_prior_year
- multi_stage: true
- sql: "{revenue}"
- type: number
- time_shift:
- - time_dimension: calendar.CalendarDate
- interval: 1 year
- type: prior
-
-- name: revenue_prior_year_ytd
- multi_stage: true
- sql: "{revenue_ytd}"
- type: number
- time_shift:
- - time_dimension: calendar.CalendarDate
- interval: 1 year
- type: prior
+- name: revenue_ytd
+ sql: revenue
+ type: sum
+ rolling_window:
+ type: to_date
+ granularity: year
+
+- name: revenue_qtd
+ sql: revenue
+ type: sum
+ rolling_window:
+ type: to_date
+ granularity: quarter
+
+- name: revenue_mtd
+ sql: revenue
+ type: sum
+ rolling_window:
+ type: to_date
+ granularity: month
```
### Example
@@ -255,41 +274,31 @@ cubes:
type: time
measures:
- - name: revenue
- sql: revenue
- type: sum
-
- name: revenue_ytd
sql: revenue
type: sum
rolling_window:
type: to_date
granularity: year
-
- - name: revenue_prior_year
- multi_stage: true
- sql: "{revenue}"
- type: number
- time_shift:
- - time_dimension: time
- interval: 1 year
- type: prior
-
- - name: revenue_prior_year_ytd
- multi_stage: true
- sql: "{revenue_ytd}"
- type: number
- time_shift:
- - time_dimension: time
- interval: 1 year
- type: prior
+
+ - name: revenue_qtd
+ sql: revenue
+ type: sum
+ rolling_window:
+ type: to_date
+ granularity: quarter
+
+ - name: revenue_mtd
+ sql: revenue
+ type: sum
+ rolling_window:
+ type: to_date
+ granularity: month
```
-Queries and results:
-
-
+Query and result:
-
+
## Fixed dimension
@@ -459,4 +468,6 @@ Query and result:
[ref-measures]: /product/data-modeling/concepts#measures
[ref-dimensions]: /product/data-modeling/concepts#dimensions
[ref-rolling-window]: /product/data-modeling/reference/measures#rolling_window
-[link-cte]: https://en.wikipedia.org/wiki/Hierarchical_and_recursive_queries_in_SQL#Common_table_expression
\ No newline at end of file
+[link-cte]: https://en.wikipedia.org/wiki/Hierarchical_and_recursive_queries_in_SQL#Common_table_expression
+[ref-ref-time-shift]: /product/data-modeling/reference/measures#time_shift
+[ref-calendar-cubes]: /product/data-modeling/concepts/calendar-cubes
diff --git a/docs/pages/product/data-modeling/recipes/period-over-period.mdx b/docs/pages/product/data-modeling/recipes/period-over-period.mdx
index fb807b71e0fa2..aceb93f6fcef6 100644
--- a/docs/pages/product/data-modeling/recipes/period-over-period.mdx
+++ b/docs/pages/product/data-modeling/recipes/period-over-period.mdx
@@ -10,26 +10,26 @@ revenue, etc.
In Cube, calculating a period-over-period metric involves the following
steps:
-- Define a couple of [`rolling_window` measures][ref-rolling-window] with
-different windows, i.e., one for _this period_ and the other for the
-_previous period_.
+- Define a [multi-stage measure][ref-multi-stage] for the _current period_.
+- Define a [time-shift measure][link-time-shift] that references the current
+period measure and shifts it to the _previous period_.
- Define a [calculated measure][ref-calculated-measure] that references
-these `rolling_window` measures and uses them in a calculation, e.g.,
-divides or subtracts them.
+these measures and uses them in a calculation, e.g., divides or subtracts them.
-Tesseract, the [next-generation data modeling engine][link-tesseract],
-provides a more efficient way to calculate [period-over-period changes][link-period-over-period].
-Tesseract is currently in preview. Use the `CUBEJS_TESSERACT_SQL_PLANNER`
-environment variable to enable it.
+Multi-stage calculations are powered by Tesseract, the [next-generation data modeling
+engine][link-tesseract]. Tesseract is currently in preview. Use the
+`CUBEJS_TESSERACT_SQL_PLANNER` environment variable to enable it.
The following data model allows to calculate a month-over-month change of
-some value. `current_month_sum` and `previous_month_sum` measures define
-two rolling windows and the `month_over_month_ratio` measure divides
-their values:
+some value. `current_month_sum` is the base measure, `previous_month_sum`
+is a time-shift measure that shifts the current month data to the previous
+month, and the `month_over_month_ratio` measure divides their values:
+
+
```yaml
cubes:
@@ -53,22 +53,68 @@ cubes:
- name: current_month_sum
sql: value
type: sum
- rolling_window:
- trailing: 1 month
- offset: end
- name: previous_month_sum
- sql: value
- type: sum
- rolling_window:
- trailing: 1 month
- offset: start
+ multi_stage: true
+ sql: "{current_month_sum}"
+ type: number
+ time_shift:
+ - interval: 1 month
+ type: prior
- name: month_over_month_ratio
- sql: "{current_month_sum} / {previous_month_sum}"
+ multi_stage: true
+ sql: "{current_month_sum} / NULLIF({previous_month_sum}, 0)"
type: number
```
+```javascript
+cube(`month_over_month`, {
+ sql: `
+ SELECT 1 AS value, '2024-01-01'::TIMESTAMP AS date UNION ALL
+ SELECT 2 AS value, '2024-01-01'::TIMESTAMP AS date UNION ALL
+ SELECT 3 AS value, '2024-02-01'::TIMESTAMP AS date UNION ALL
+ SELECT 4 AS value, '2024-02-01'::TIMESTAMP AS date UNION ALL
+ SELECT 5 AS value, '2024-03-01'::TIMESTAMP AS date UNION ALL
+ SELECT 6 AS value, '2024-03-01'::TIMESTAMP AS date UNION ALL
+ SELECT 7 AS value, '2024-04-01'::TIMESTAMP AS date UNION ALL
+ SELECT 8 AS value, '2024-04-01'::TIMESTAMP AS date
+ `,
+
+ dimensions: {
+ date: {
+ sql: `date`,
+ type: `time`
+ }
+ },
+
+ measures: {
+ current_month_sum: {
+ sql: `value`,
+ type: `sum`
+ },
+
+ previous_month_sum: {
+ multi_stage: true,
+ sql: `${current_month_sum}`,
+ type: `number`,
+ time_shift: [{
+ interval: `1 month`,
+ type: `prior`
+ }]
+ },
+
+ month_over_month_ratio: {
+ multi_stage: true,
+ sql: `${current_month_sum} / NULLIF(${previous_month_sum}, 0)`,
+ type: `number`
+ }
+ }
+})
+```
+
+
+
## Result
Often, when calculating period-over-period changes, you would also use a
@@ -81,23 +127,24 @@ that matches the period, i.e., `month` for month-over-month calculations:
{
"dimension": "month_over_month.date",
"granularity": "month",
- "dateRange": "this year"
+ "dateRange": ["2024-01-01", "2025-01-01"]
}
],
"measures": [
"month_over_month.current_month_sum",
"month_over_month.previous_month_sum",
- "month_over_month.change"
+ "month_over_month.month_over_month_ratio"
]
}
```
Here's the result:
-
+
+
-[ref-rolling-window]: /product/data-modeling/reference/measures#rolling_window
+[ref-multi-stage]: /product/data-modeling/concepts/multi-stage-calculations
[ref-calculated-measure]: /product/data-modeling/overview#4-using-calculated-measures
[ref-time-dimension-granularity]: /product/apis-integrations/rest-api/query-format#time-dimensions-format
[link-tesseract]: https://cube.dev/blog/introducing-next-generation-data-modeling-engine
-[link-period-over-period]: /product/data-modeling/concepts/multi-stage-calculations#prior-date
\ No newline at end of file
+[link-time-shift]: /product/data-modeling/concepts/multi-stage-calculations#time-shift
diff --git a/docs/pages/product/data-modeling/reference/cube.mdx b/docs/pages/product/data-modeling/reference/cube.mdx
index e85105401c4c2..c43ae050bf0ef 100644
--- a/docs/pages/product/data-modeling/reference/cube.mdx
+++ b/docs/pages/product/data-modeling/reference/cube.mdx
@@ -593,6 +593,16 @@ cubes:
+### `calendar`
+
+The `calendar` parameter is used to mark [calendar cubes][ref-calendar-cubes].
+It's set to `false` by default.
+
+When set to `true`, Cube will treat this cube as a calendar cube and allow to
+[override time-shifts][ref-calendar-cubes-time-shifts] and [granularities][ref-calendar-cubes-granularities]
+on its [time dimensions][ref-time-dimensions]. This can be useful for [time-shift
+calculations][ref-time-shift] with a custom calendar.
+
### `pre_aggregations`
The `pre_aggregations` parameter is used to configure [pre-aggregations][ref-ref-pre-aggs].
@@ -636,4 +646,9 @@ The `access_policy` parameter is used to configure [data access policies][ref-re
[ref-ref-pre-aggs]: /product/data-modeling/reference/pre-aggregations
[ref-ref-dap]: /product/data-modeling/reference/data-access-policies
[ref-syntax-cube-sql]: /product/data-modeling/syntax#cubesql-function
-[ref-extension]: /product/data-modeling/concepts/code-reusability-extending-cubes
\ No newline at end of file
+[ref-extension]: /product/data-modeling/concepts/code-reusability-extending-cubes
+[ref-calendar-cubes]: /product/data-modeling/concepts/calendar-cubes
+[ref-calendar-cubes-time-shifts]: /product/data-modeling/concepts/calendar-cubes#time-shifts
+[ref-calendar-cubes-granularities]: /product/data-modeling/concepts/calendar-cubes#granularities
+[ref-time-dimensions]: /product/data-modeling/concepts#time-dimensions
+[ref-time-shift]: /product/data-modeling/concepts/multi-stage-calculations#time-shift
\ No newline at end of file
diff --git a/docs/pages/product/data-modeling/reference/dimensions.mdx b/docs/pages/product/data-modeling/reference/dimensions.mdx
index d8bcc24d68f00..e4d088b76dfe3 100644
--- a/docs/pages/product/data-modeling/reference/dimensions.mdx
+++ b/docs/pages/product/data-modeling/reference/dimensions.mdx
@@ -148,6 +148,42 @@ cube(`products`, {
})
```
+### `title`
+
+You can use the `title` parameter to change a dimension's displayed name. By
+default, Cube will humanize your dimension key to create a display name. In
+order to override default behavior, please use the `title` property:
+
+
+
+```javascript
+cube(`products`, {
+ // ...
+
+ dimensions: {
+ meta_value: {
+ title: `Size`,
+ sql: `meta_value`,
+ type: `string`
+ }
+ }
+})
+```
+
+```yaml
+cubes:
+ - name: products
+ # ...
+
+ dimensions:
+ - name: meta_value
+ title: Size
+ sql: meta_value
+ type: string
+```
+
+
+
### `description`
This parameter provides a human-readable description of a dimension.
@@ -184,6 +220,42 @@ cubes:
+### `public`
+
+The `public` parameter is used to manage the visibility of a dimension. Valid
+values for `public` are `true` and `false`. When set to `false`, this dimension
+**cannot** be queried through the API. Defaults to `true`.
+
+
+
+```javascript
+cube(`products`, {
+ // ...
+
+ dimensions: {
+ comment: {
+ sql: `comment`,
+ type: `string`,
+ public: false
+ }
+ }
+})
+```
+
+```yaml
+cubes:
+ - name: products
+ # ...
+
+ dimensions:
+ - name: comment
+ sql: comment
+ type: string
+ public: false
+```
+
+
+
### `format`
`format` is an optional parameter. It is used to format the output of dimensions
@@ -419,42 +491,6 @@ cubes:
-### `public`
-
-The `public` parameter is used to manage the visibility of a dimension. Valid
-values for `public` are `true` and `false`. When set to `false`, this dimension
-**cannot** be queried through the API. Defaults to `true`.
-
-
-
-```javascript
-cube(`products`, {
- // ...
-
- dimensions: {
- comment: {
- sql: `comment`,
- type: `string`,
- public: false
- }
- }
-})
-```
-
-```yaml
-cubes:
- - name: products
- # ...
-
- dimensions:
- - name: comment
- sql: comment
- type: string
- public: false
-```
-
-
-
### `sql`
`sql` is a required parameter. It can take any valid SQL expression depending on
@@ -525,42 +561,6 @@ cubes:
-### `title`
-
-You can use the `title` parameter to change a dimension's displayed name. By
-default, Cube will humanize your dimension key to create a display name. In
-order to override default behavior, please use the `title` property:
-
-
-
-```javascript
-cube(`products`, {
- // ...
-
- dimensions: {
- meta_value: {
- title: `Size`,
- sql: `meta_value`,
- type: `string`
- }
- }
-})
-```
-
-```yaml
-cubes:
- - name: products
- # ...
-
- dimensions:
- - name: meta_value
- title: Size
- sql: meta_value
- type: string
-```
-
-
-
### `type`
`type` is a required parameter. There are various types that can be assigned to
@@ -700,6 +700,193 @@ cube(`orders`, {
+#### Calendar cubes
+
+When the `granularities` parameter is used in time dimensions within [calendar
+cubes][ref-calendar-cubes], you can still use it to define custom granularities.
+
+Additionally, you can override the _default granularities_. This can be useful for
+modeling custom calendars, such as fiscal calendars.
+
+
+
+```yaml
+cubes:
+ - name: fiscal_calendar
+ calendar: true
+ sql: >
+ SELECT
+ date_key,
+ calendar_date,
+ start_of_month,
+ start_of_quarter,
+ start_of_year
+ FROM calendar_table
+
+ dimensions:
+ - name: date_key
+ sql: date_key
+ type: time
+ primary_key: true
+
+ - name: date
+ sql: calendar_date
+ type: time
+ granularities:
+ - name: month
+ sql: "{CUBE}.start_of_month"
+
+ - name: quarter
+ sql: "{CUBE}.start_of_quarter"
+
+ - name: year
+ sql: "{CUBE}.start_of_year"
+
+```
+
+```javascript
+cube(`fiscal_calendar`, {
+ calendar: true,
+ sql: `
+ SELECT
+ date_key,
+ calendar_date,
+ start_of_month,
+ start_of_quarter,
+ start_of_year
+ FROM calendar_table
+ `,
+
+ dimensions: {
+ date_key: {
+ sql: `date_key`,
+ type: `time`,
+ primary_key: true
+ },
+
+ date: {
+ sql: `calendar_date`,
+ type: `time`,
+ granularities: {
+ month: {
+ sql: `${CUBE}.start_of_month`
+ },
+ quarter: {
+ sql: `${CUBE}.start_of_quarter`
+ },
+ year: {
+ sql: `${CUBE}.start_of_year`
+ }
+ }
+ }
+ }
+})
+```
+
+
+
+### `time_shift`
+
+The `time_shift` parameter allows overriding the time shift behavior for time dimensions
+within [calendar cubes][ref-calendar-cubes]. Such time shifts can be referenced in
+[time-shift measures][ref-time-shift] of other cubes, enabling the use of custom calendars.
+
+The `time_shift` parameter can only be set on _time dimensions_ within _calendar cubes_,
+i.e., cubes where the [`calendar` parameter][ref-cube-calendar] is set to `true`.
+
+The `time_shift` parameter accepts an array of time shift definitions. Each definition
+can include `time_dimension`, `type`, `interval`, and `name` parameters, similarly to the
+[`time_shift` parameter][ref-measure-time-shift] of time-shift measures. Additionally,
+you can use the `sql` parameter to define a custom time mapping using a SQL expression.
+
+
+
+```yaml
+cubes:
+ - name: fiscal_calendar
+ calendar: true
+ sql: >
+ SELECT
+ date_key,
+ calendar_date,
+ fiscal_date_prior_year,
+ fiscal_date_next_quarter
+ FROM calendar_table
+
+ dimensions:
+ - name: date_key
+ sql: date_key
+ type: time
+ primary_key: true
+
+ - name: date
+ sql: calendar_date
+ type: time
+ time_shift:
+ - name: prior_calendar_year
+ type: prior
+ interval: 1 year
+
+ - name: next_calendar_quarter
+ type: next
+ interval: 1 quarter
+
+ - name: prior_fiscal_year
+ sql: "{CUBE}.fiscal_date_prior_year"
+
+ - name: next_fiscal_quarter
+ sql: "{CUBE}.fiscal_date_next_quarter"
+```
+
+```javascript
+cube(`fiscal_calendar`, {
+ calendar: true,
+ sql: `
+ SELECT
+ date_key,
+ calendar_date,
+ fiscal_date_prior_year,
+ fiscal_date_next_quarter
+ FROM calendar_table
+ `,
+
+ dimensions: {
+ date_key: {
+ sql: `date_key`,
+ type: `time`,
+ primary_key: true
+ },
+
+ date: {
+ sql: `calendar_date`,
+ type: `time`,
+ time_shift: [
+ {
+ name: `prior_calendar_year`,
+ type: `prior`,
+ interval: `1 year`
+ },
+ {
+ name: `next_calendar_quarter`,
+ type: `next`,
+ interval: `1 quarter`
+ },
+ {
+ name: `prior_fiscal_year`,
+ sql: `${CUBE}.fiscal_date_prior_year`
+ },
+ {
+ name: `next_fiscal_quarter`,
+ sql: `${CUBE}.fiscal_date_next_quarter`
+ }
+ ]
+ }
+ }
+});
+```
+
+
+
[ref-ref-cubes]: /product/data-modeling/reference/cube
[ref-schema-ref-joins]: /product/data-modeling/reference/joins
@@ -716,4 +903,8 @@ cube(`orders`, {
[link-date-time-string]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date#date_time_string_format
[ref-custom-granularity-recipe]: /product/data-modeling/recipes/custom-granularity
[ref-ref-hierarchies]: /product/data-modeling/reference/hierarchies
-[ref-data-sources]: /product/configuration/data-sources
\ No newline at end of file
+[ref-data-sources]: /product/configuration/data-sources
+[ref-calendar-cubes]: /product/data-modeling/concepts/calendar-cubes
+[ref-time-shift]: /product/data-modeling/concepts/multi-stage-calculations#time-shift
+[ref-cube-calendar]: /product/data-modeling/reference/cube#calendar
+[ref-measure-time-shift]: /product/data-modeling/reference/measures#time_shift
diff --git a/docs/pages/product/data-modeling/reference/measures.mdx b/docs/pages/product/data-modeling/reference/measures.mdx
index fea84705e73d9..7de4a76b86a21 100644
--- a/docs/pages/product/data-modeling/reference/measures.mdx
+++ b/docs/pages/product/data-modeling/reference/measures.mdx
@@ -50,6 +50,42 @@ cubes:
+### `title`
+
+You can use the `title` parameter to change a measure’s displayed name. By
+default, Cube will humanize your measure key to create a display name. In order
+to override default behavior, please use the `title` parameter.
+
+
+
+```javascript
+cube(`orders`, {
+ // ...
+
+ measures: {
+ orders_count: {
+ title: `Number of Orders Placed`,
+ sql: `id`,
+ type: `count`
+ }
+ }
+})
+```
+
+```yaml
+cubes:
+ - name: orders
+ # ...
+
+ measures:
+ - name: orders_count
+ title: Number of Orders Placed
+ sql: id
+ type: count
+```
+
+
+
### `description`
This parameter provides a human-readable description of a measure.
@@ -86,14 +122,45 @@ cubes:
-### `drill_members`
+### `public`
-Using the `drill_members` parameter, you can define a set of [drill
-down][ref-drilldowns] fields for the measure. `drill_members` is defined as an
-array of dimensions. Cube automatically injects dimensions’ names and other
-cubes’ names with dimensions in the context, so you can reference these
-variables in the `drill_members` array. [Learn more about how to define and use
-drill downs][ref-drilldowns].
+The `public` parameter is used to manage the visibility of a measure. Valid
+values for `public` are `true` and `false`. When set to `false`, this measure
+**cannot** be queried through the API. Defaults to `true`.
+
+
+
+```javascript
+cube(`orders`, {
+ // ...
+
+ measures: {
+ orders_count: {
+ sql: `id`,
+ type: `count`,
+ public: false
+ }
+ }
+})
+```
+
+```yaml
+cubes:
+ - name: orders
+ # ...
+
+ measures:
+ - name: orders_count
+ sql: id
+ type: count
+ public: false
+```
+
+
+
+### `meta`
+
+Custom metadata. Can be used to pass any information to the frontend.
@@ -105,7 +172,9 @@ cube(`orders`, {
revenue: {
type: `sum`,
sql: `price`,
- drill_members: [id, price, status, products.name, products.id]
+ meta: {
+ any: "value"
+ }
}
}
})
@@ -120,20 +189,18 @@ cubes:
- name: revenue
type: sum
sql: price
- drill_members:
- - id
- - price
- - status
- - products.name
- - products.id
+ meta:
+ any: value
```
-### `filters`
+### `sql`
-If you want to add some conditions for a metric's calculation, you should use
-the `filters` parameter. The syntax looks like the following:
+`sql` is a required parameter. It can take any valid SQL expression depending on
+the `type` of the measure. Please refer to the [Measure Types
+Guide][ref-schema-ref-types-formats-measures-types] for detailed information on
+the corresponding `sql` parameter.
@@ -142,10 +209,9 @@ cube(`orders`, {
// ...
measures: {
- orders_completed_count: {
- sql: `id`,
- type: `count`,
- filters: [{ sql: `${CUBE}.status = 'completed'` }]
+ users_count: {
+ sql: `COUNT(*)`,
+ type: `number`
}
}
})
@@ -157,21 +223,25 @@ cubes:
# ...
measures:
- - name: orders_completed_count
- sql: id
- type: count
- filters:
- - sql: "{CUBE}.status = 'completed'"
+ - name: users_count
+ sql: "COUNT(*)"
+ type: number
```
-### `format`
+Depending on the measure [type](#type), the `sql` parameter would either:
+* Be skipped (in case of the `count` type).
+* Contain an aggregate function, e.g., `STRING_AGG(string_dimension, ',')`
+(in case of `string`, `time`, `boolean`, and `number` types).
+* Contain a non-aggregated expression that Cube would wrap into an aggregate
+function according to the measure type (in case of the `avg`, `count_distinct`,
+`count_distinct_approx`, `min`, `max`, and `sum` types).
-`format` is an optional parameter. It is used to format the output of measures
-in different ways, for example, as currency for `revenue`. Please refer to the
-[Measure Formats][ref-schema-ref-types-formats-measures-formats] for the full
-list of supported formats.
+### `filters`
+
+If you want to add some conditions for a metric's calculation, you should use
+the `filters` parameter. The syntax looks like the following:
@@ -180,10 +250,10 @@ cube(`orders`, {
// ...
measures: {
- total: {
- sql: `amount`,
- type: `sum`,
- format: `currency`
+ orders_completed_count: {
+ sql: `id`,
+ type: `count`,
+ filters: [{ sql: `${CUBE}.status = 'completed'` }]
}
}
})
@@ -195,17 +265,21 @@ cubes:
# ...
measures:
- - name: total
- sql: amount
- type: sum
- format: currency
+ - name: orders_completed_count
+ sql: id
+ type: count
+ filters:
+ - sql: "{CUBE}.status = 'completed'"
```
-### `meta`
+### `type`
-Custom metadata. Can be used to pass any information to the frontend.
+`type` is a required parameter. There are various types that can be assigned to
+a measure. Please refer to the [Measure
+Types][ref-schema-ref-types-formats-measures-types] for the full list of measure
+types.
@@ -214,12 +288,9 @@ cube(`orders`, {
// ...
measures: {
- revenue: {
- type: `sum`,
- sql: `price`,
- meta: {
- any: "value"
- }
+ orders_count: {
+ sql: `id`,
+ type: `count`
}
}
})
@@ -231,11 +302,9 @@ cubes:
# ...
measures:
- - name: revenue
- type: sum
- sql: price
- meta:
- any: value
+ - name: orders_count
+ sql: id
+ type: count
```
@@ -348,127 +417,442 @@ cubes:
-### `public`
+### `multi_stage`
-The `public` parameter is used to manage the visibility of a measure. Valid
-values for `public` are `true` and `false`. When set to `false`, this measure
-**cannot** be queried through the API. Defaults to `true`.
+The `multi_stage` parameter is used to define measures that are used with [multi-stage
+calculations][ref-multi-stage], e.g., [time-shift measures][ref-time-shift].
+```yaml
+cubes:
+ - name: time_shift
+ sql: >
+ SELECT '2024-01-01'::TIMESTAMP AS time, 100 AS revenue UNION ALL
+ SELECT '2024-02-01'::TIMESTAMP AS time, 200 AS revenue UNION ALL
+ SELECT '2024-03-01'::TIMESTAMP AS time, 300 AS revenue UNION ALL
+
+ SELECT '2025-01-01'::TIMESTAMP AS time, 400 AS revenue UNION ALL
+ SELECT '2025-02-01'::TIMESTAMP AS time, 500 AS revenue UNION ALL
+ SELECT '2025-03-01'::TIMESTAMP AS time, 600 AS revenue
+
+ dimensions:
+ - name: time
+ sql: time
+ type: time
+
+ measures:
+ - name: revenue
+ sql: revenue
+ type: sum
+
+ - name: revenue_prior_year
+ multi_stage: true
+ sql: "{revenue}"
+ type: number
+ time_shift:
+ - time_dimension: time
+ interval: 1 year
+ type: prior
+```
+
```javascript
-cube(`orders`, {
- // ...
+cube(`time_shift`, {
+ sql: `
+ SELECT '2024-01-01'::TIMESTAMP AS time, 100 AS revenue UNION ALL
+ SELECT '2024-02-01'::TIMESTAMP AS time, 200 AS revenue UNION ALL
+ SELECT '2024-03-01'::TIMESTAMP AS time, 300 AS revenue UNION ALL
+
+ SELECT '2025-01-01'::TIMESTAMP AS time, 400 AS revenue UNION ALL
+ SELECT '2025-02-01'::TIMESTAMP AS time, 500 AS revenue UNION ALL
+ SELECT '2025-03-01'::TIMESTAMP AS time, 600 AS revenue
+ `,
+
+ dimensions: {
+ time: {
+ sql: `time`,
+ type: `time`
+ }
+ },
measures: {
- orders_count: {
- sql: `id`,
- type: `count`,
- public: false
+ revenue: {
+ sql: `revenue`,
+ type: `sum`
+ },
+
+ revenue_prior_year: {
+ multi_stage: true,
+ sql: `${revenue}`,
+ type: `number`,
+ time_shift: [
+ {
+ time_dimension: `time`,
+ interval: `1 year`,
+ type: `prior`
+ }
+ ]
}
}
})
```
-```yaml
-cubes:
- - name: orders
- # ...
+
- measures:
- - name: orders_count
- sql: id
- type: count
- public: false
-```
+### `time_shift`
-
+The `time_shift` parameter is used to configure a [time shift][ref-time-shift] for a
+measure. It accepts an array of time shift configurations that consist of `time_dimension`,
+`type`, `interval`, and `name` parameters.
-### `sql`
+#### `type` and `interval`
-`sql` is a required parameter. It can take any valid SQL expression depending on
-the `type` of the measure. Please refer to the [Measure Types
-Guide][ref-schema-ref-types-formats-measures-types] for detailed information on
-the corresponding `sql` parameter.
+These parameters define the time shift direction and size. The `type` can be either
+`prior` (shifting time backwards) or `next` (shifting time forwards).
+The `interval` parameter defines the size of the time shift and has the following format:
+`quantity unit`, e.g., `1 year` or `7 days`.
-```javascript
-cube(`orders`, {
- // ...
+```yaml
+ measures:
+ - name: revenue
+ sql: revenue
+ type: sum
+
+ - name: revenue_7d_ago
+ multi_stage: true
+ sql: "{revenue}"
+ type: number
+ time_shift:
+ - interval: 7 days
+ type: prior
+
+ - name: revenue_1y_ago
+ multi_stage: true
+ sql: "{revenue}"
+ type: number
+ time_shift:
+ - interval: 1 year
+ type: prior
+```
+```javascript
measures: {
- users_count: {
- sql: `COUNT(*)`,
- type: `number`
+ revenue: {
+ sql: `revenue`,
+ type: `sum`
+ },
+
+ revenue_7d_ago: {
+ multi_stage: true,
+ sql: `${revenue}`,
+ type: `number`,
+ time_shift: [
+ {
+ interval: `7 days`,
+ type: `prior`
+ }
+ ]
+ },
+
+ revenue_1y_ago: {
+ multi_stage: true,
+ sql: `${revenue}`,
+ type: `number`,
+ time_shift: [
+ {
+ interval: `1 year`,
+ type: `prior`
+ }
+ ]
}
}
-})
```
-```yaml
-cubes:
- - name: orders
- # ...
+
+
+#### `time_dimension`
+
+The `time_dimension` parameter is used to specify the time dimension for the time shift.
+If it's omitted, Cube will apply the time shift to all time dimensions in the query.
+In this case, only single time shift configuration is allowed in `time_shift`.
+
+If `time_dimension` is specified, the time shift will only happen if the query contains
+this very time dimension. This is useful if you'd like to apply different time shifts to
+different time dimensions or if you want to apply a time shift only when a specific time
+dimension is present in the query.
+
+
+```yaml
measures:
- - name: users_count
- sql: "COUNT(*)"
+ - name: revenue
+ sql: revenue
+ type: sum
+
+ - name: lagging_revenue
+ multi_stage: true
+ sql: "{revenue}"
type: number
+ time_shift:
+ - time_dimension: purchase_date
+ interval: 3 months
+ type: prior
+
+ - time_dimension: shipping_date
+ interval: 2 months
+ type: prior
+
+ - time_dimension: delivery_date
+ interval: 1 month
+ type: prior
```
-
+```javascript
+ measures: {
+ revenue: {
+ sql: `revenue`,
+ type: `sum`
+ },
-Depending on the measure [type](#type), the `sql` parameter would either:
-* Be skipped (in case of the `count` type).
-* Contain an aggregate function, e.g., `STRING_AGG(string_dimension, ',')`
-(in case of `string`, `time`, `boolean`, and `number` types).
-* Contain a non-aggregated expression that Cube would wrap into an aggregate
-function according to the measure type (in case of the `avg`, `count_distinct`,
-`count_distinct_approx`, `min`, `max`, and `sum` types).
+ lagging_revenue: {
+ multi_stage: true,
+ sql: `${revenue}`,
+ type: `number`,
+ time_shift: [
+ {
+ time_dimension: `purchase_date`,
+ interval: `3 months`,
+ type: `prior`
+ },
+ {
+ time_dimension: `shipping_date`,
+ interval: `2 months`,
+ type: `prior`
+ },
+ {
+ time_dimension: `delivery_date`,
+ interval: `1 month`,
+ type: `prior`
+ }
+ ]
+ }
+ }
+```
-### `title`
+
-You can use the `title` parameter to change a measure’s displayed name. By
-default, Cube will humanize your measure key to create a display name. In order
-to override default behavior, please use the `title` parameter.
+#### `name`
+
+The `name` parameter is used to reference a _named time shift_ that is defined on a time
+dimension from a [calendar cube][ref-calendar-cubes]. Named time shifts are used in cases
+when different measures use the same time shift configuration (e.g., `prior` + `1 year`)
+but have to be shifted differently depending on the custom calendar.
+```yaml
+cubes:
+ - name: sales_calendar
+ calendar: true
+ sql: >
+ SELECT '2025-06-02Z' AS date, '2024-06-01Z' AS mapped_date, '2024-06-03Z' AS mapped_date_alt UNION ALL
+ SELECT '2025-06-03Z' AS date, '2024-06-02Z' AS mapped_date, '2024-06-04Z' AS mapped_date_alt UNION ALL
+ SELECT '2025-06-04Z' AS date, '2024-06-03Z' AS mapped_date, '2024-06-05Z' AS mapped_date_alt UNION ALL
+ SELECT '2025-06-05Z' AS date, '2024-06-04Z' AS mapped_date, '2024-06-06Z' AS mapped_date_alt UNION ALL
+ SELECT '2025-06-06Z' AS date, '2024-06-05Z' AS mapped_date, '2024-06-07Z' AS mapped_date_alt UNION ALL
+ SELECT '2025-06-07Z' AS date, '2024-06-06Z' AS mapped_date, '2024-06-08Z' AS mapped_date_alt UNION ALL
+ SELECT '2025-06-08Z' AS date, '2024-06-07Z' AS mapped_date, '2024-06-09Z' AS mapped_date_alt
+
+ dimensions:
+ - name: date_key
+ sql: "{CUBE}.date::TIMESTAMP"
+ type: time
+ primary_key: true
+
+ - name: date
+ sql: "{CUBE}.date::TIMESTAMP"
+ type: time
+ time_shift:
+ - name: 1_year_prior
+ sql: "{CUBE}.mapped_date::TIMESTAMP"
+
+ - name: 1_year_prior_alternative
+ sql: "{CUBE}.mapped_date_alt::TIMESTAMP"
+
+ - name: sales
+ sql: >
+ SELECT 101 AS id, '2024-06-01Z' AS date, 101 AS amount UNION ALL
+ SELECT 102 AS id, '2024-06-02Z' AS date, 102 AS amount UNION ALL
+ SELECT 103 AS id, '2024-06-03Z' AS date, 103 AS amount UNION ALL
+ SELECT 104 AS id, '2024-06-04Z' AS date, 104 AS amount UNION ALL
+ SELECT 105 AS id, '2024-06-05Z' AS date, 105 AS amount UNION ALL
+ SELECT 106 AS id, '2024-06-06Z' AS date, 106 AS amount UNION ALL
+ SELECT 107 AS id, '2024-06-07Z' AS date, 107 AS amount UNION ALL
+ SELECT 108 AS id, '2024-06-08Z' AS date, 108 AS amount UNION ALL
+ SELECT 109 AS id, '2024-06-09Z' AS date, 109 AS amount UNION ALL
+
+ SELECT 202 AS id, '2025-06-02Z' AS date, 202 AS amount UNION ALL
+ SELECT 203 AS id, '2025-06-03Z' AS date, 203 AS amount UNION ALL
+ SELECT 204 AS id, '2025-06-04Z' AS date, 204 AS amount UNION ALL
+ SELECT 205 AS id, '2025-06-05Z' AS date, 205 AS amount UNION ALL
+ SELECT 206 AS id, '2025-06-06Z' AS date, 206 AS amount UNION ALL
+ SELECT 207 AS id, '2025-06-07Z' AS date, 207 AS amount UNION ALL
+ SELECT 208 AS id, '2025-06-08Z' AS date, 208 AS amount
+
+ joins:
+ - name: sales_calendar
+ sql: "{sales.date} = {sales_calendar.date_key}"
+ relationship: many_to_one
+
+ dimensions:
+ - name: id
+ sql: id
+ type: number
+ primary_key: true
+
+ - name: date
+ sql: "{CUBE}.date::TIMESTAMP"
+ type: time
+ public: false
+
+ measures:
+ - name: total_amount
+ sql: amount
+ type: sum
+
+ - name: total_amount_1y_prior
+ multi_stage: true
+ sql: "{total_amount}"
+ type: number
+ time_shift:
+ - name: 1_year_prior
+
+ - name: total_amount_1y_prior_alternative
+ multi_stage: true
+ sql: "{total_amount}"
+ type: number
+ time_shift:
+ - name: 1_year_prior_alternative
+```
+
```javascript
-cube(`orders`, {
- // ...
+cube(`sales_calendar`, {
+ sql: `
+ SELECT '2025-06-02Z' AS date, '2024-06-01Z' AS mapped_date, '2024-06-03Z' AS mapped_date_alt UNION ALL
+ SELECT '2025-06-03Z' AS date, '2024-06-02Z' AS mapped_date, '2024-06-04Z' AS mapped_date_alt UNION ALL
+ SELECT '2025-06-04Z' AS date, '2024-06-03Z' AS mapped_date, '2024-06-05Z' AS mapped_date_alt UNION ALL
+ SELECT '2025-06-05Z' AS date, '2024-06-04Z' AS mapped_date, '2024-06-06Z' AS mapped_date_alt UNION ALL
+ SELECT '2025-06-06Z' AS date, '2024-06-05Z' AS mapped_date, '2024-06-07Z' AS mapped_date_alt UNION ALL
+ SELECT '2025-06-07Z' AS date, '2024-06-06Z' AS mapped_date, '2024-06-08Z' AS mapped_date_alt UNION ALL
+ SELECT '2025-06-08Z' AS date, '2024-06-07Z' AS mapped_date, '2024-06-09Z' AS mapped_date_alt
+ `,
+
+ dimensions: {
+ date_key: {
+ sql: `${CUBE}.date::TIMESTAMP`,
+ type: `time`,
+ primary_key: true
+ },
- measures: {
- orders_count: {
- title: `Number of Orders Placed`,
- sql: `id`,
- type: `count`
+ date: {
+ sql: `${CUBE}.date::TIMESTAMP`,
+ type: `time`,
+ time_shift: [
+ {
+ name: `1_year_prior`,
+ sql: `${CUBE}.mapped_date::TIMESTAMP`
+ },
+ {
+ name: `1_year_prior_alternative`,
+ sql: `${CUBE}.mapped_date_alt::TIMESTAMP`
+ }
+ ]
}
}
})
-```
-```yaml
-cubes:
- - name: orders
- # ...
+cube(`sales`, {
+ sql: `
+ SELECT 101 AS id, '2024-06-01Z' AS date, 101 AS amount UNION ALL
+ SELECT 102 AS id, '2024-06-02Z' AS date, 102 AS amount UNION ALL
+ SELECT 103 AS id, '2024-06-03Z' AS date, 103 AS amount UNION ALL
+ SELECT 104 AS id, '2024-06-04Z' AS date, 104 AS amount UNION ALL
+ SELECT 105 AS id, '2024-06-05Z' AS date, 105 AS amount UNION ALL
+ SELECT 106 AS id, '2024-06-06Z' AS date, 106 AS amount UNION ALL
+ SELECT 107 AS id, '2024-06-07Z' AS date, 107 AS amount UNION ALL
+ SELECT 108 AS id, '2024-06-08Z' AS date, 108 AS amount UNION ALL
+ SELECT 109 AS id, '2024-06-09Z' AS date, 109 AS amount UNION ALL
+
+ SELECT 202 AS id, '2025-06-02Z' AS date, 202 AS amount UNION ALL
+ SELECT 203 AS id, '2025-06-03Z' AS date, 203 AS amount UNION ALL
+ SELECT 204 As id, '2025-06-04Z' As date, 204 As amount UNION ALL
+ SELECT 205 As id, '2025-06-05Z' As date, 205 As amount UNION ALL
+ SELECT 206 As id, '2025-06-06Z' As date, 206 As amount UNION ALL
+ SELECT 207 As id, '2025-06-07Z' As date, 207 As amount UNION ALL
+ SELECT 208 As id, '2025-06-08Z' As date, 208 As amount
+ `,
+
+ joins: {
+ sales_calendar: {
+ sql: `${sales}.date = ${sales_calendar}.date_key`,
+ relationship: `many_to_one`
+ }
+ },
- measures:
- - name: orders_count
- title: Number of Orders Placed
- sql: id
- type: count
+ dimensions: {
+ id: {
+ sql: `id`,
+ type: `number`,
+ primary_key: true
+ },
+
+ date: {
+ sql: `${CUBE}.date::TIMESTAMP`,
+ type: `time`,
+ public: false
+ }
+ },
+
+ measures: {
+ total_amount: {
+ sql: `amount`,
+ type: `sum`
+ },
+
+ total_amount_1y_prior: {
+ multi_stage: true,
+ sql: `${total_amount}`,
+ type: `number`,
+ time_shift: [{
+ name: `1_year_prior`
+ }]
+ },
+
+ total_amount_1y_prior_alternative: {
+ multi_stage: true,
+ sql: `${total_amount}`,
+ type: `number`,
+ time_shift: [{
+ name: `1_year_prior_alternative`
+ }]
+ }
+ }
+)
```
-### `type`
+Named time shifts also allow to reuse the same time shift configuration across multiple
+measures and cubes where they are defined.
-`type` is a required parameter. There are various types that can be assigned to
-a measure. Please refer to the [Measure
-Types][ref-schema-ref-types-formats-measures-types] for the full list of measure
-types.
+### `format`
+
+`format` is an optional parameter. It is used to format the output of measures
+in different ways, for example, as currency for `revenue`. Please refer to the
+[Measure Formats][ref-schema-ref-types-formats-measures-formats] for the full
+list of supported formats.
@@ -477,9 +861,10 @@ cube(`orders`, {
// ...
measures: {
- orders_count: {
- sql: `id`,
- type: `count`
+ total: {
+ sql: `amount`,
+ type: `sum`,
+ format: `currency`
}
}
})
@@ -491,18 +876,22 @@ cubes:
# ...
measures:
- - name: orders_count
- sql: id
- type: count
+ - name: total
+ sql: amount
+ type: sum
+ format: currency
```
-## Calculated measures
+### `drill_members`
-In the case where you need to specify a formula for measure calculating with
-other measures, you can compose a formula in `sql`. For example, to calculate
-the conversion of buyers of all users.
+Using the `drill_members` parameter, you can define a set of [drill
+down][ref-drilldowns] fields for the measure. `drill_members` is defined as an
+array of dimensions. Cube automatically injects dimensions’ names and other
+cubes’ names with dimensions in the context, so you can reference these
+variables in the `drill_members` array. [Learn more about how to define and use
+drill downs][ref-drilldowns].
@@ -511,10 +900,10 @@ cube(`orders`, {
// ...
measures: {
- purchases_to_created_account_ratio: {
- sql: `${purchases} / ${users.count} * 100.0`,
- type: `number`,
- format: `percent`
+ revenue: {
+ type: `sum`,
+ sql: `price`,
+ drill_members: [id, price, status, products.name, products.id]
}
}
})
@@ -526,17 +915,19 @@ cubes:
# ...
measures:
- - name: purchases_to_created_account_ratio
- sql: "{purchases} / {users.count} * 100.0"
- type: number
- format: percent
+ - name: revenue
+ type: sum
+ sql: price
+ drill_members:
+ - id
+ - price
+ - status
+ - products.name
+ - products.id
```
-You can create calculated measures from several joined cubes. In this case, a
-join will be created automatically.
-
[ref-ref-cubes]: /product/data-modeling/reference/cube
[ref-schema-ref-types-formats-measures-types]:
@@ -548,4 +939,7 @@ join will be created automatically.
[ref-playground]: /product/workspace/playground
[ref-apis]: /product/apis-integrations
[ref-rolling-window]: /product/data-modeling/concepts/multi-stage-calculations#rolling-window
-[link-tesseract]: https://cube.dev/blog/introducing-next-generation-data-modeling-engine
\ No newline at end of file
+[link-tesseract]: https://cube.dev/blog/introducing-next-generation-data-modeling-engine
+[ref-multi-stage]: /product/data-modeling/concepts/multi-stage-calculations
+[ref-time-shift]: /product/data-modeling/concepts/multi-stage-calculations#time-shift
+[ref-calendar-cubes]: /product/data-modeling/concepts/calendar-cubes
diff --git a/docs/pages/product/data-modeling/reference/types-and-formats.mdx b/docs/pages/product/data-modeling/reference/types-and-formats.mdx
index 89b67d765b582..095490a054e41 100644
--- a/docs/pages/product/data-modeling/reference/types-and-formats.mdx
+++ b/docs/pages/product/data-modeling/reference/types-and-formats.mdx
@@ -133,9 +133,8 @@ cubes:
### `number`
-The `number` type is usually used, when performing arithmetic operations on
-arithmetic operations on measures. [Learn more about Calculated
-Measures][ref-schema-ref-calc-measures].
+The `number` type is usually used, when performing arithmetic operations on measures,
+e.g., in [calculated measures][ref-calculated-measures].
The `sql` parameter is required and must include any valid SQL expression
with an aggregate function that returns a value of the numeric type.
@@ -1002,6 +1001,5 @@ cubes:
[ref-string-time-dims]: /product/data-modeling/recipes/string-time-dimensions
[ref-schema-ref-preaggs-rollup]:
/product/data-modeling/reference/pre-aggregations#rollup
-[ref-schema-ref-calc-measures]:
- /product/data-modeling/reference/measures#calculated-measures
+[ref-calculated-measures]: /product/data-modeling/concepts/calculated-members#calculated-measures
[ref-drilldowns]: /product/apis-integrations/recipes/drilldowns