Sundial
Semantic Layer

Semantic Model

The unit that binds one warehouse table to its dimensions, measures, and grain.

A semantic model binds one warehouse table (or view) to a named set of dimensions and measures. There is one model per source table. Every query is rooted in one or more models.

Minimal example

apiVersion: context-engine/v1
kind: SemanticModel
metadata:
  id: 7d4c1e2f-1a3b-4c8e-9f10-2a3b4c5d6e7f
  name: orders
spec:
  asset_qualified_name: ANALYTICS.PUBLIC.FCT_ORDERS
  description: Per-order facts. One row per order_id.
  grain:
    - [order_id]
  default_time_dimension: order_date
  dimensions:
    - name: order_id
      type: categorical
      description: Unique order identifier (the table's grain).
    - name: order_date
      type: time
      sql: ordered_at
      time_grains: [day, week, month]
      description: Date the order was placed.
    - name: order_status
      type: categorical
      sql: status
      description: Order lifecycle state.
  measures:
    - name: order_count
      measure_type: aggregate
      agg: count
      sql: order_id
      description: Number of orders.
    - name: order_total
      measure_type: aggregate
      agg: sum
      sql: order_amount
      description: Sum of order amounts.

spec fields

FieldRequiredNotes
asset_qualified_nameYes, unless the model is baselessWarehouse-native three-part name (DATABASE.SCHEMA.TABLE) of the table this model sits on. Omit it only for a baseless model — one with no base table, served entirely by pre-aggregations.
descriptionYesOne paragraph describing the grain and what the table represents. Used to ground AI-generated queries.
grainYesList of column-name lists; each inner list is one valid unique-key combination (e.g. [[order_id]], [[user_id, day]]). Every column named must be a dimension on this model. See Grain.
default_time_dimensionYesThe time dimension used when a query doesn't specify one. Must reference a time dimension on this model.
dimensionsYes (≥ 1)See Dimensions.
measuresOptional (default empty)A model may have only dimensions — e.g. a lookup table used purely for joins. See Measures.
labelOptionalDisplay name.
ownerOptionalTeam or person.
tagsOptionalList of strings for organizing and filtering.

The model's name lives at metadata.name — do not also write spec.name.

Grain

grain is the column combination that is unique across every row of the table — the true unique key, not just "the columns that describe a row" (those are dimensions). Declare it accurately: too broad a key double-counts your measures, and a grain that doesn't line up across tables prevents joins that should work.

Foreign-key columns (e.g. customer_id on an orders table) are declared as categorical dimensions but are not part of the grain — they are join keys to other models, not this table's unique key.

Joins are grain-based

There are no entity types and no explicit join declarations. Two models join automatically when a dimension on one shares a name with a dimension on the other at a compatible grain — the layer aggregates each model to the shared dimension and combines the results.

When authoring:

  • To make two models joinable on customer_id, declare a dimension named customer_id on both. If the names don't match, they simply won't join.
  • Changing a model's grain (e.g. [[user_id]][[user_id, day]]) can change which joins are possible, so it may affect cross-model calculated measures that relied on the old grain.

Cross-model calculated measures (e.g. orders.revenue / inventory.cost) rely on this same shared-dimension join — see Measures → Calculated.


Still have questions?

On this page