Sales Model Walkthrough¶
This example walks through a complete semantic model for a sales analytics use case with customers, products, and orders.
The Model¶
# yaml-language-server: $schema=schema/obml-schema.json
version: 1.0
dataObjects:
Customers:
code: CUSTOMERS
database: WAREHOUSE
schema: PUBLIC
columns:
Customer ID:
code: CUSTOMER_ID
abstractType: string
Customer Name:
code: NAME
abstractType: string
Country:
code: COUNTRY
abstractType: string
Segment:
code: SEGMENT
abstractType: string
Products:
code: PRODUCTS
database: WAREHOUSE
schema: PUBLIC
columns:
Product ID:
code: PRODUCT_ID
abstractType: string
Product Name:
code: NAME
abstractType: string
Category:
code: CATEGORY
abstractType: string
Orders:
code: ORDERS
database: WAREHOUSE
schema: PUBLIC
columns:
Order ID:
code: ORDER_ID
abstractType: string
Order Date:
code: ORDER_DATE
abstractType: date
Customer ID:
code: CUSTOMER_ID
abstractType: string
Product ID:
code: PRODUCT_ID
abstractType: string
Quantity:
code: QUANTITY
abstractType: int
Price:
code: PRICE
abstractType: float
joins:
- joinType: many-to-one
joinTo: Customers
columnsFrom:
- Customer ID
columnsTo:
- Customer ID
- joinType: many-to-one
joinTo: Products
columnsFrom:
- Product ID
columnsTo:
- Product ID
dimensions:
Customer Country:
dataObject: Customers
column: Country
resultType: string
Customer Segment:
dataObject: Customers
column: Segment
resultType: string
Product Name:
dataObject: Products
column: Product Name
resultType: string
Product Category:
dataObject: Products
column: Category
resultType: string
Order Date:
dataObject: Orders
column: Order Date
resultType: date
timeGrain: month
measures:
Revenue:
resultType: float
aggregation: sum
expression: '{[Orders].[Price]} * {[Orders].[Quantity]}'
Order Count:
columns:
- dataObject: Orders
column: Order ID
resultType: int
aggregation: count
Average Order Value:
resultType: float
aggregation: avg
expression: '{[Orders].[Price]} * {[Orders].[Quantity]}'
metrics:
Revenue per Order:
expression: '{[Revenue]} / {[Order Count]}'
Data Objects Explained¶
Star Schema Structure¶
This model follows a classic star schema:
graph LR
O["<b>Orders</b><br/><i>fact</i><br/>Order ID<br/>Order Date<br/>Customer ID<br/>Product ID<br/>Quantity<br/>Price"]
C["<b>Customers</b><br/>Customer ID<br/>Name<br/>Country<br/>Segment"]
P["<b>Products</b><br/>Product ID<br/>Name<br/>Category"]
O -->|many-to-one| C
O -->|many-to-one| P
Orders is the fact table — it declares joins to both dimension tables. The compiler automatically identifies it as the base object because it has join definitions.
Column Mapping¶
Each column maps a business name to a physical column:
| Data Object | Business Name | Physical Column |
|---|---|---|
| Customers | Customer ID | CUSTOMER_ID |
| Customers | Country | COUNTRY |
| Products | Product Name | NAME |
| Orders | Price | PRICE |
The physical column name appears in generated SQL, while the business name is used in dimensions, measures, and queries.
Dimensions Explained¶
Standard Dimension¶
References the Country column in the Customers data object. When queried, generates "Customers"."COUNTRY" with a GROUP BY.
Time Dimension¶
The timeGrain: month means queries using this dimension will apply date_trunc('month', ...) by default.
Measures Explained¶
Expression Measure — Revenue¶
Revenue:
resultType: float
aggregation: sum
expression: '{[Orders].[Price]} * {[Orders].[Quantity]}'
Compiles to: SUM("Orders"."PRICE" * "Orders"."QUANTITY")
The expression references columns using {[DataObject].[Column]} syntax. The sum aggregation wraps the entire expression.
Simple Measure — Order Count¶
Compiles to: COUNT("Orders"."ORDER_ID")
Expression Measure — Average Order Value¶
Average Order Value:
resultType: float
aggregation: avg
expression: '{[Orders].[Price]} * {[Orders].[Quantity]}'
Compiles to: AVG("Orders"."PRICE" * "Orders"."QUANTITY")
Uses {[DataObject].[Column]} syntax to reference columns directly in the expression — no columns array needed.
Metrics Explained¶
This divides Revenue by Order Count. The {[Revenue]} and {[Order Count]} placeholders reference the measures defined above by name. Each measure is computed independently and then combined in the metric expression.
Example Queries¶
Revenue by Country¶
query = QueryObject(
select=QuerySelect(
dimensions=["Customer Country"],
measures=["Revenue", "Order Count"],
),
order_by=[QueryOrderBy(field="Revenue", direction=SortDirection.DESC)],
limit=1000,
)
Generated SQL (Postgres):
SELECT
"Customers"."COUNTRY" AS "Customer Country",
SUM("Orders"."PRICE" * "Orders"."QUANTITY") AS "Revenue",
COUNT("Orders"."ORDER_ID") AS "Order Count"
FROM WAREHOUSE.PUBLIC.ORDERS AS "Orders"
LEFT JOIN WAREHOUSE.PUBLIC.CUSTOMERS AS "Customers"
ON "Orders"."CUSTOMER_ID" = "Customers"."CUSTOMER_ID"
GROUP BY "Customers"."COUNTRY"
ORDER BY "Revenue" DESC
LIMIT 1000
Filtered Query — SMB Customers¶
query = QueryObject(
select=QuerySelect(
dimensions=["Customer Country"],
measures=["Revenue"],
),
where=[
QueryFilter(field="Customer Segment", op=FilterOperator.IN, value=["SMB", "MidMarket"]),
],
)
Generated SQL:
SELECT
"Customers"."COUNTRY" AS "Customer Country",
SUM("Orders"."PRICE" * "Orders"."QUANTITY") AS "Revenue"
FROM WAREHOUSE.PUBLIC.ORDERS AS "Orders"
LEFT JOIN WAREHOUSE.PUBLIC.CUSTOMERS AS "Customers"
ON "Orders"."CUSTOMER_ID" = "Customers"."CUSTOMER_ID"
WHERE ("Customers"."SEGMENT" IN ('SMB', 'MidMarket'))
GROUP BY "Customers"."COUNTRY"
Multi-Dimension Query¶
query = QueryObject(
select=QuerySelect(
dimensions=["Customer Country", "Product Category"],
measures=["Revenue"],
),
)
Generated SQL:
SELECT
"Customers"."COUNTRY" AS "Customer Country",
"Products"."CATEGORY" AS "Product Category",
SUM("Orders"."PRICE" * "Orders"."QUANTITY") AS "Revenue"
FROM WAREHOUSE.PUBLIC.ORDERS AS "Orders"
LEFT JOIN WAREHOUSE.PUBLIC.CUSTOMERS AS "Customers"
ON "Orders"."CUSTOMER_ID" = "Customers"."CUSTOMER_ID"
LEFT JOIN WAREHOUSE.PUBLIC.PRODUCTS AS "Products"
ON "Orders"."PRODUCT_ID" = "Products"."PRODUCT_ID"
GROUP BY "Customers"."COUNTRY", "Products"."CATEGORY"
Notice how the compiler automatically adds the Products join because the Product Category dimension requires it.