Documentation
Data modeling
Concepts

Concepts

Cube borrows a lot of terminology from data science and OLAP theory (opens in a new tab), and this document is intended for both newcomers and regular users to refresh their understanding.

We'll use a sample e-commerce database with two tables, orders and line_items to illustrate the concepts throughout this page:

orders

idstatuscompleted_atcreated_at
1completed2019-01-05 00:00:00.0000002019-01-02 00:00:00.000000
2shipped2019-01-17 00:00:00.0000002019-01-02 00:00:00.000000
3completed2019-01-27 00:00:00.0000002019-01-02 00:00:00.000000
4shipped2019-01-09 00:00:00.0000002019-01-02 00:00:00.000000
5processing2019-01-29 00:00:00.0000002019-01-02 00:00:00.000000

line_items

idproduct_idorder_idquantitypricecreated_at
131112752019-01-31 00:00:00.000000
249262482021-01-20 00:00:00.000000
389361972021-11-25 00:00:00.000000
471482232019-12-23 00:00:00.000000
56455752019-04-20 00:00:00.000000

Cubes

A cube represents a dataset in Cube, and is conceptually similar to a view in SQL (opens in a new tab). 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:

YAML
JavaScript
cubes:
  - name: orders
    sql_table: orders

You can also use the sql property to accommodate more complex SQL queries:

YAML
JavaScript
cubes:
  - name: orders
    sql: >
      SELECT *
      FROM orders, line_items
      WHERE orders.id = line_items.order_id
 

Within each cube are definitions of dimensions, measures, and segments. Joins are used to define relations between cubes; pre-aggregations are designed to accelerate queries to cubes.

Note that cubes support extension, polymorphism, and data blending. Also, cubes should not necessarily be defined statically; you can actually build dynamic data models.

Cube supports data models that consist of thousands of cubes and views. For massive multi-tenancy configurations, e.g., with more than 1,000 tenants, consider using a multi-cluster deployment.

Views

Views sit on top of the data graph of cubes and create a facade of your whole data model with which data consumers can interact. They are useful for defining metrics, managing governance and data access, and controlling ambiguous join paths.

Views can not have their own members. Instead, use the cubes or includes parameters to include measures and dimensions from other cubes into the view. In the example below, we create the orders view which includes select members from base_orders, products, and users cubes:

YAML
JavaScript
views:
  - name: orders
 
    cubes:
      - join_path: base_orders
        includes:
          - status
          - created_date
          - total_amount
          - total_amount_shipped
          - count
          - average_order_value
 
      - join_path: base_orders.line_items.products
        includes:
          - name: name
            alias: product
 
      - join_path: base_orders.users
        prefix: true
        includes: "*"
        excludes:
          - company

Views do not define any pre-aggregations. Instead, they reuse pre-aggregations from underlying cubes.

View may not only be defined statically; you can actually build dynamic data models.

Dimensions

Dimensions represent the properties of a single data point in the cube. The orders table contains only dimensions, so representing them in the orders cube is straightforward:

YAML
JavaScript
cubes:
  - name: orders
    # ...
 
    dimensions:
      - name: id
        sql: id
        type: number
        # Here we explicitly let Cube know this field is the primary key
        # This is required for de-duplicating results when using joins
        primary_key: true
 
      - name: status
        sql: status
        type: string

Note that the id dimension is defined as a primary key. It is also possible to have more than one primary key dimension in a cube if you'd like them all to be parts of a composite key.

The line_items table also has a couple of dimensions which can be represented as follows:

YAML
JavaScript
cubes:
  - name: line_items
    # ...
 
    dimensions:
      - name: id
        sql: id
        type: number
        # Again, we explicitly let Cube know this field is the primary key
        # This is required for de-duplicating results when using joins
        primary_key: true
 
      - name: order_id
        sql: order_id
        type: number

Also, subquery dimensions can be used to join cubes implicitly.

Dimension types

Dimensions can be of different types. See the dimension type reference for details.

Time dimensions

Time-based properties are modeled using dimensions of the time type. They allow grouping the result set by a unit of time (e.g., days, weeks, month, etc.), also known as the time dimension granularity.

The following granularities are available by default for any time dimension: year, quarter, month, week (starting on Monday), day, hour, minute, second. You can also define custom granularities and optionally expose them via proxy dimensions in case you need to use weeks starting on Sunday, fiscal years, etc.

See this recipe for more custom granularity examples.

Here's how we can add time dimensions to the data model:

YAML
JavaScript
cubes:
  - name: orders
    # ...
 
    dimensions:
      - name: created_at
        sql: created_at
        type: time
        # You can use this time dimension with all default granularities:
        # year, quarter, month, week, day, hour, minute, second
 
      - name: completed_at
        sql: completed_at
        type: time
        # You can use this time dimension with all default granularities
        # and an additional custom granularity defined below
        granularities:
          - name: fiscal_year_starting_on_february_01
            interval: 1 year
            offset: 1 month

Time dimensions are essential to enabling performance boosts such as partitioned pre-aggregations and incremental refreshes.

Measures

Measures represent the properties of a set of data points in the cube. To add a measure called count to our orders cube, for example, we can do the following:

YAML
JavaScript
cubes:
  - name: orders
    # ...
 
    measures:
      - name: count
        type: count

In our LineItems cube, we can also create a measure to sum up the total value of line items sold:

YAML
JavaScript
cubes:
  - name: line_items
    # ...
 
    measures:
      - name: total
        sql: price
        type: sum

Measure types

Measures can be of different types. See the measure type reference for details.

Often, aggregate functions in SQL are mapped to measure types in the following way:

Aggregate function in SQLMeasure type in Cube
AVGavg
BOOL_AND, BOOL_ORboolean
COUNTcount
COUNT(DISTINCT …)count_distinct
APPROX_COUNT_DISTINCTcount_distinct_approx
MAXmax
MINmin
PERCENTILE_CONT, MEDIANnumber
STRING_AGG, LISTAGGstring
SUMsum
Any function returning a timestamp, e.g., MAX(time)time

Measure additivity

Additivity is a property of measures that detemines whether measure values, once calculated for a set of dimensions, can be further aggregated to calculate measure values for a subset of these dimensions.

Measure additivity has an impact on pre-aggregation matching.

Additivity of a measure depends on its type. Only measures with the following types are considered additive: count, count_distinct_approx, min, max, sum. Measures with all other types are considered non-additive.

Example

Consider the following cube:

YAML
JavaScript
cubes:
  - name: employees
    sql: >
      SELECT 1 AS id, 'Ali' AS first_name, 20 AS age, 'Los Gatos' AS city UNION ALL
      SELECT 2 AS id, 'Bob' AS first_name, 30 AS age, 'San Diego' AS city UNION ALL
      SELECT 3 AS id, 'Eve' AS first_name, 40 AS age, 'San Diego' AS city
 
    measures:
      - name: count
        type: count
 
      - name: avg_age
        sql: age
        type: avg
 
    dimensions:
      - name: city
        sql: city
        type: string

If we run a query that includes city as a dimension and count and avg_age as measures, we'll get the following results:

citycountavg_age
Los Gatos120
San Diego235

Then, if we remove the city dimension from the query, we'll get the following results:

countavg_age
330

As you can see, the value of the count measure that we've got for the second query could have been calculated based on the results of the first one: 1 + 2 = 3. It explains why the count measure, having the count type, is considered additive.

However, the value of the avg_age measure that we've got for the second query can't be calculated based on the results of the first one: there's no way to derive 30 from 20 and 35. This is why the avg_age measure, having the avg type, is considered non-additive.

Leaf measures

Measures that do not reference other measures are considered leaf measures.

By definition, all measures that only reference SQL columns and expressions are leaf measures. On the other hand, calculated measures might not necessarily be leaf measures because they can reference other measures.

Whether a query contains only additive leaf measures has an impact on pre-aggregation matching.

Joins

Joins define the relationships between cubes, which then allows accessing and comparing properties from two or more cubes at the same time. In Cube, all joins are LEFT JOINs.

An INNER JOIN can be replicated with Cube; when making a Cube query, add a filter for IS NOT NULL on the required column.

In the following example, we are left-joining the line_items cube onto our orders cube:

YAML
JavaScript
cubes:
  - name: orders
    # ...
 
    joins:
      - name: line_items
        # Here we use the `CUBE` global to refer to the current cube,
        # so the following is equivalent to `orders.id = line_items.order_id`
        sql: "{CUBE}.id = {line_items.order_id}"
        relationship: many_to_one

There are three types of join relationships (one_to_one, one_to_many, and many_to_one) and a few other concepts.

Segments

Segments are filters that are predefined in the data model instead of a Cube query. They allow simplifying Cube queries and make it easy to re-use common filters across a variety of queries.

To add a segment which limits results to completed orders, we can do the following:

YAML
JavaScript
cubes:
  - name: orders
    # ...
 
    segments:
      - name: only_completed
        sql: "{CUBE}.status = 'completed'"

Pre-aggregations

Pre-aggregations are a powerful way of caching frequently-used, expensive queries and keeping the cache up-to-date on a periodic basis. Within a data model, they are defined under the pre_aggregations property:

YAML
JavaScript
cubes:
  - name: orders
    # ...
 
    pre_aggregations:
      - name: main
        measures:
          - count
        dimensions:
          - status
        time_dimension: created_at
        granularity: day

A more thorough introduction can be found in Getting Started with Pre-Aggregations.