The structure
Grain: one row per product per sales transaction (one receipt line). Every dimension has an Unknown member with key -1. Anonymous walk-in sales point to it, so they never drop out of inner joins.
Concepts in 60 seconds
quantity and net_amount can be summed across every dimension. A semi-additive measure, such as an inventory balance, can't be summed across time. Ratios and unit_price are non-additive: store the parts and divide after aggregating.sku and customer_id are source-system natural keys. The warehouse adds its own integer surrogate key, which keeps joins fast and stable, separates you from source changes and makes SCD2 possible.-1 row labelled "Unknown". Totals stay correct, inner joins are safe, and data quality issues stay visible.effective_from/to + is_current, so old facts keep the attributes that were valid when they happened.dim_date, dim_product and so on are shared by every fact table (sales, returns, inventory). That lets you compare different processes side by side. This shared set is Kimball's "bus matrix".transaction_id is a dimension key with no attributes, so it lives in the fact table. You still use it to group lines into baskets.dim_date can play several roles, such as order date, ship date and delivery date, through separate foreign keys or views like dim_order_date.Tables & columns
fact_sales 47,290 rows
transaction_id | BIGINT | DD |
date_key | BIGINT | FK |
product_key | BIGINT | FK |
customer_key | BIGINT | FK |
store_key | BIGINT | FK |
quantity | BIGINT | measure · additive |
unit_price | DOUBLE | measure · non-additive |
discount_amount | DOUBLE | measure · additive |
net_amount | DOUBLE | measure · additive |
cost_amount | DOUBLE | measure · additive |
dim_date 732 rows
date_key | BIGINT | PK · SK |
full_date | DATE | NK |
day_of_month | BIGINT | |
iso_week | BIGINT | |
month | BIGINT | |
quarter | BIGINT | |
year | BIGINT | |
weekday_name | VARCHAR | |
weekday_num | BIGINT | |
is_holiday | BOOLEAN | |
fiscal_year | BIGINT | |
fiscal_period | BIGINT | |
year_month | VARCHAR |
dim_product 62 rows
product_key | BIGINT | PK · SK |
sku | VARCHAR | NK |
product_name | VARCHAR | |
category | VARCHAR | |
subcategory | VARCHAR | |
brand | VARCHAR | |
list_price | DOUBLE | |
unit_cost | DOUBLE | |
effective_from | DATE | SCD2 |
effective_to | DATE | SCD2 |
is_current | BOOLEAN | SCD2 |
Try it: SQL console
DuckDB runs inside this page and reads the Parquet files above. Pick an example or write your own query. Tables: fact_sales, dim_date, dim_product, dim_customer, dim_store.
In a BI tool or Grafana
BI tools like Power BI, Tableau, Looker or Superset are built for stars. You import the fact and dimension tables, define relationships on the surrogate keys and write measures such as SUM(net_amount) once. Users then drag attributes from the dimensions.
Grafana has no data source that understands star schemas. You point a SQL data source (PostgreSQL, ClickHouse, Snowflake, BigQuery, DuckDB/MotherDuck…) at the mart and write the join yourself. For time series, select d.full_date AS time, a metric and an optional series column, and use $__timeFilter(d.full_date) and template variables, such as WHERE p.category IN ($category), fed from SELECT DISTINCT category FROM dim_product. A tidy pattern is to create a flat view joining the fact to its dimensions, then build panels on that view. For reusable, governed metrics, put a semantic layer like Cube or dbt Semantic Layer between Grafana and the star.