Modern analytics systems are not built directly on raw SQL tables anymore. Instead, teams define a semantic layer that gives business meaning to raw data.
In Cube, the core building block of this semantic layer is called a Cube.
A cube represents a business entity such as:
· Orders
· Customers
· Restaurants
· Payments
· Drivers
Each cube maps to a table (or SQL query) in your database and defines:
· metrics
· dimensions
· joins
· business rules
· aggregations
Instead of every dashboard or AI application writing SQL manually, they query these reusable business definitions.
1. Why Cubes Matter?
Imagine multiple teams writing analytics queries directly on raw tables. One dashboard calculates revenue using "subtotal_amount + tax_amount", another uses "final_amount" and other excludes cancelled orders.
Now every report shows different numbers. A semantic layer solves this problem by centralizing business logic in one place.
With cubes:
· revenue is defined once
· delivery metrics are standardized
· joins become reusable
· AI tools can safely generate analytics
· dashboards become consistent
2. Our Example Dataset
For this tutorial, we’ll use the orders table from our food delivery platform schema. The table stores food delivery transactions:
| Column | Meaning | | ----------------------- | --------------------------- | | order_id | Unique order | | customer_id | Customer who placed order | | restaurant_id | Restaurant fulfilling order | | order_status | Delivery state | | final_amount | Total order value | | ordered_at | Order timestamp | | actual_delivery_minutes | Delivery duration |
3. Creating Your First Cube
In Cube, a cube usually maps to a database table using sql_table. Here is a beginner friendly cube definition for the orders table.
cubes: - name: orders sql_table: orders
This tells Cube to create a semantic model named orders using the raw table orders.
At this point:
· Cube knows the source table
· but it still doesn’t know what metrics exist, which columns are dimensions how to aggregate data etc.,
That is where dimensions and measures come in.
3.1 Adding Dimensions
Dimensions describe the properties of a data record. Think of dimensions as:
· labels
· categories
· attributes
· fields you group or filter by
Examples:
· order status
· city
· customer
· restaurant
· date
cubes: - name: orders sql_table: orders dimensions: - name: order_id sql: order_id type: number primary_key: true - name: order_status sql: order_status type: string - name: ordered_at sql: ordered_at type: time
a. dimensions
This section contains all dimension definitions for the cube. Think of it as, these are the fields that describe the data. For the orders table, dimensions might include:
· order_id
· order_status
· ordered_at
· customer_id
· restaurant_id
Dimensions are typically:
· identifiers
· categories
· timestamps
· descriptive attributes
b. name: order_id
This defines the semantic name of the dimension inside Cube. This is the name users and tools will query. In the example:
· orders → cube name
· order_id → dimension name
The name does NOT have to match the database column name.
For example:
- name: id sql: order_id
This would expose the field as orders.id, even though the database column is order_id.
The semantic name allows:
· cleaner APIs
· business-friendly naming
· abstraction from physical schemas
· easier refactoring later
c. sql: order_id
This tells Cube to use the order_id column from the database table. This is the actual SQL expression used in generated queries.
Cube internally generates SQL like:
SELECT order_id FROM orders
SQL Property Can Be More Than a Column
It can also contain:
· SQL expressions
· CASE statements
· calculations
· concatenations
Example:
sql: CONCAT('ORD-', order_id) sql: CASE WHEN final_amount > 1000 THEN 'High Value' ELSE 'Regular' END
d. type: number
This tells Cube the data type of the dimension. Cube needs this information to:
· generate correct SQL
· validate queries
· enable filtering
· optimize aggregations
· power BI integrations
Following table summarizes common dimension types.
|
Type |
Meaning |
|
string |
Text values |
|
number |
Numeric values |
|
boolean |
True/False |
|
time |
Date/Timestamp |
|
geo |
Geographic data |
What Happens if Type is Wrong?
Suppose you define "type: string", instead of number. Following potential problems will occur:
· incorrect filtering behavior
· inefficient SQL generation
· broken BI expectations
· sorting issues
Correct typing is important in semantic modeling.
e. primary_key: true
This tells Cube that, this dimension uniquely identifies each row in this cube.
For the orders table:
· every order has a unique order_id
· no duplicates exist
· each row represents exactly one order
Why Primary Keys Are Extremely Important?
Primary keys help Cube:
· understand row uniqueness
· generate correct joins
· prevent duplicate aggregation
· optimize queries
· build pre-aggregations safely
In summary, this configuration means create a numeric dimension called order_id using the database column order_id, and treat it as the unique identifier for each order
3.2 Adding Measures
Measures define aggregated business metrics.
Measures answer questions like:
· How many orders?
· Total revenue?
· Average delivery time?
· Cancellation rate?
Example Measure
cubes: - name: orders sql_table: orders measures: - name: total_orders type: count - name: total_revenue sql: final_amount type: sum - name: average_delivery_time sql: actual_delivery_minutes type: avg
Understanding Each Measure
a. Count Measure
- name: total_orders type: count
This generates SQL like COUNT(*), Useful for:
· order counts
· daily activity
· operational dashboards
b. Sum Measure
- name: total_revenue sql: final_amount type: sum
This calculates SUM(final_amount), Useful for:
· GMV
· revenue analytics
· financial dashboards
c. Average Measure
- name: average_delivery_time sql: actual_delivery_minutes type: avg
This calculates AVG(actual_delivery_minutes), Useful for:
· SLA tracking
· logistics analytics
· operational monitoring
Here’s the complete cube:
cubes: - name: orders sql_table: orders dimensions: - name: order_id sql: order_id type: number primary_key: true - name: order_status sql: order_status type: string - name: ordered_at sql: ordered_at type: time measures: - name: total_orders type: count - name: total_revenue sql: final_amount type: sum - name: average_delivery_time sql: actual_delivery_minutes type: avg
3.3 What Happens Behind the Scenes?
When BI tools or AI agents query Cube:
· they do not directly query PostgreSQL
· they query the semantic layer
Cube then:
· understands the business model
· generates optimized SQL
· applies business rules
· handles joins
· manages caching and pre-aggregations
This creates:
· consistency
· governance
· reusable analytics
· AI-friendly data access
4. How do Applications actally use Cube?
The next important question is, how do applications actually query this cube? and more importantly "How does Cube generate SQL automatically?""
This is where the semantic layer becomes extremely powerful.
Applications do NOT send raw SQL to the database. Instead, they send a query specification to Cube.
Cube:
· understands the semantic model
· validates the query
· generates optimized SQL
· executes the SQL
· returns analytics results
This abstraction is the heart of semantic modeling.
4.1 What is a Query Specification?
A query specification is a structured request describing:
· which measures to calculate
· which dimensions to group by
· filters
· time ranges
· sorting
· limits
Instead of writing SQL manually, clients send JSON or YAML like query objects.
Example 1: Total revenue grouped by order status
Query Specification
measures: - orders.total_revenue dimensions: - orders.order_status
How Cube understand this?
Cube maps orders.total_revenue to
sql: final_amount type: sum
and orders.order_status to
sql: order_status
Generated SQL looks like below.
SELECT "orders".order_status "orders__order_status", sum("orders".final_amount) "orders__total_revenue" FROM orders AS "orders" GROUP BY 1 ORDER BY 2 DESC
Example 2: Time-Series Query Example
Suppose we want Monthly revenue trend
measures: - orders.total_revenue timeDimensions: - dimension: orders.ordered_at granularity: month
Cube sees:
· ordered_at is a time dimension
· grouping granularity = month
· measure = revenue sum
Generated SQL
SELECT date_trunc('month', ("orders".ordered_at::timestamptz AT TIME ZONE 'UTC')) "orders__ordered_at_month", sum("orders".final_amount) "orders__total_revenue" FROM orders AS "orders" GROUP BY 1 ORDER BY 1 ASC
Cube automatically supports:
· year
· quarter
· month
· week
· day
· hour
· minute
without additional SQL.
Example 3: Filtering Example
Suppose we only want delivered orders.
measures: - orders.total_revenue filters: - member: orders.order_status operator: equals values: - DELIVERED
Generated SQL
SELECT sum("orders".final_amount) "orders__total_revenue" FROM orders AS "orders" WHERE ("orders".order_status = 'DELIVERED')
Example 4: Combining Dimensions, Time, and Filters
Suppose we want monthly delivered-order revenue by status
measures: - orders.total_revenue dimensions: - orders.order_status timeDimensions: - dimension: orders.ordered_at granularity: month filters: - member: orders.order_status operator: equals values: - DELIVERED
Generated SQL
SELECT "orders".order_status "orders__order_status", date_trunc('month', ("orders".ordered_at::timestamptz AT TIME ZONE 'UTC')) "orders__ordered_at_month", sum("orders".final_amount) "orders__total_revenue" FROM orders AS "orders" WHERE ("orders".order_status = 'DELIVERED') GROUP BY 1, 2 ORDER BY 2 ASC
Example 5: Sorting Example
Suppose we want top statuses by revenue
measures: - orders.total_revenue dimensions: - orders.order_status order: orders.total_revenue: desc
Generate SQL like below.
SELECT "orders".order_status "orders__order_status", sum("orders".final_amount) "orders__total_revenue" FROM orders AS "orders" GROUP BY 1 ORDER BY 2 DESC
Example 7: Limiting Results
Suppose we want only the top 5 rows.
dimensions: - orders.order_id - orders.ordered_at limit: 5
Generated SQL
SELECT "orders".order_id "orders__order_id", "orders".ordered_at "orders__ordered_at" FROM orders AS "orders" GROUP BY 1, 2 ORDER BY 1 ASC LIMIT 5
Example 8: Here is a more realistic analytics request.
measures: - orders.total_orders - orders.total_revenue - orders.average_delivery_time dimensions: - orders.order_status timeDimensions: - dimension: orders.ordered_at granularity: month filters: - member: orders.order_status operator: notEquals values: - CANCELLED order: orders.total_revenue: desc limit: 10
Generated SQL Query
SELECT "orders".order_status "orders__order_status", date_trunc('month', ("orders".ordered_at::timestamptz AT TIME ZONE 'UTC')) "orders__ordered_at_month", count("orders".order_id) "orders__total_orders", sum("orders".final_amount) "orders__total_revenue", avg("orders".actual_delivery_minutes) "orders__average_delivery_time" FROM orders AS "orders" WHERE ("orders".order_status <> 'CANCELLED' OR "orders".order_status IS NULL) GROUP BY 1, 2 ORDER BY 2 ASC LIMIT 10
5. Why Semantic Query Specs Are Powerful?
Applications no longer need to:
· write SQL
· understand joins
· manage aggregations
· handle time grouping
· optimize queries
The semantic layer handles all of that automatically.
Following table summarizes the benefits of Query Specifications.
|
Feature |
Benefit |
|
Measures |
Standardized business metrics |
|
Dimensions |
Reusable grouping attributes |
|
Time Dimensions |
Automatic time-series analytics |
|
Filters |
Safe query constraints |
|
Ordering |
Declarative sorting |
|
Limits |
Pagination and optimization |
Why This is Important for AI Analytics?
AI agents can safely generate query specifications.
measures: - orders.total_revenue dimensions: - orders.order_status
instead of attempting fragile raw SQL generation.
This makes:
· AI analytics safer
· validation easier
· governance stronger
· query generation more reliable
In summary, the semantic model defines "what the business data means". The query specification defines "what analytics we want". Cube transforms these semantic queries into optimized SQL automatically.
That separation is what makes semantic layers so powerful.
Previous Next Home
No comments:
Post a Comment