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