Chapter 3 · Foundations
Dimensional Modelling
sqlcmd running inside the SQL Server Linux container (docker exec). The T-SQL is the same as on Windows.Before writing a single load, decide what one row of each table means. This chapter walks through the SBEcoLTDDW star schema: the course's tables, the columns the pipeline adds, and the modelling decisions behind each one.
Start with the grain
The grain is one sentence saying what a single fact row represents. Everything else follows from it.
Grain of Fact.Orders
One row per online sales order of an individual US customer, at the order-header level, with the order's sub-total in OrderTotal.
Because the grain is the header and not the order line, product can't be a dimension of this table; you would need a line-level fact for that. And because OrderTotal is a plain amount, it is fully additive: you can sum it across days, customers and states and the result still means something.
Facts and dimensions
| Table | Type | Grain | Rows |
|---|---|---|---|
Fact.Orders | Transaction fact | One sales order | 9,571 |
Dimension.Date | Conformed dimension, generated | One calendar day | 7,670 |
Dimension.Customer | SCD Type 1 + 2 | One customer version | 7,821 (7,820 + Unknown) |
A quick bus matrix shows which dimensions each fact uses. It is the plan you extend when new facts arrive.
| Fact \ Dimension | Date | Customer |
|---|---|---|
| Orders | ✔ DateKey | ✔ CustomerKey (version at order time) |
Keys: surrogate, alternate, smart and degenerate
- Surrogate key
CustomerKey(IDENTITY): meaningless, owned by the warehouse. It has to be, because one source customer can have several versions, and each version needs its own key. - Alternate (business) key
CustomerAlternateKey: the source'sCustomerID, used to match incoming rows to existing versions. - Smart key
DateKey=yyyymmdd(e.g.20140423). For dates a readable key is the norm: it is stable, sortable and can be computed from the date without a lookup. - Degenerate dimension
SalesOrderNumber: an identifier with no attributes of its own, so it lives in the fact table. It doubles as the fact's business key for idempotentMERGEs, with a filtered unique index to enforce it.
The course DDL, and one fix
These are the course's Chapter 2 scripts as they run in the lab. One change was needed: Year was declared tinyint, which only holds 0–255, so any real year failed with an overflow.
-- Change: Dimension.Date.Year is smallint instead of tinyint (tinyint stops at 255, so real years fail).
USE SBEcoLTDDW;
GO
CREATE TABLE Dimension.Date (
DateKey int NOT NULL,
FullDate date NOT NULL,
DayName nvarchar(9) NOT NULL,
DayNumber tinyint NOT NULL,
MonthName varchar(9) NOT NULL,
MonthNumber tinyint NOT NULL,
QuarterNumber tinyint NOT NULL,
Year smallint NOT NULL -- course: tinyint
CONSTRAINT PK_Date PRIMARY KEY CLUSTERED (
DateKey ASC
)
);
GO
CREATE TABLE Dimension.Customer (
CustomerKey int IDENTITY (1,1) NOT NULL,
CustomerAlternateKey int NOT NULL,
FirstName nvarchar(50) NOT NULL,
LastName nvarchar(50) NOT NULL,
FullName nvarchar(100) NOT NULL,
City nvarchar(50) NOT NULL,
State char(2) NOT NULL,
Statename nvarchar(20) NOT NULL,
Zip char(5) NOT NULL
CONSTRAINT PK_Customer PRIMARY KEY CLUSTERED (
CustomerKey ASC
)
);
GO
USE SBEcoLTDDW;
GO
CREATE TABLE Fact.Orders (
OrderID int IDENTITY (1,1) NOT NULL,
DateKey int NOT NULL,
CustomerKey int NOT NULL,
OrderTotal money NOT NULL
CONSTRAINT PK_Orders PRIMARY KEY CLUSTERED (
OrderID ASC
)
);
GO
ALTER TABLE Fact.Orders WITH CHECK ADD CONSTRAINT FK_Fact_Orders_DateKey_Dimension_Date FOREIGN KEY(DateKey)
REFERENCES Dimension.Date (DateKey);
GO
ALTER TABLE Fact.Orders CHECK CONSTRAINT FK_Fact_Orders_DateKey_Dimension_Date;
GO
ALTER TABLE Fact.Orders WITH CHECK ADD CONSTRAINT FK_Fact_Orders_CustomerKey_Dimension_Customer FOREIGN KEY(CustomerKey)
REFERENCES Dimension.Customer (CustomerKey);
GO
ALTER TABLE Fact.Orders CHECK CONSTRAINT FK_Fact_Orders_CustomerKey_Dimension_Customer;
GO
Foreign keys in a warehouse
The course declares WITH CHECK foreign keys from the fact to both dimensions, and the pipeline keeps them (both are trusted: is_not_trusted = 0). They cost a little at load time and in return guarantee no orphan rows, and the optimizer can use trusted keys to remove joins. Large warehouses sometimes drop them for speed and check orphans in the DQ gate instead. SBEcoLTDDW does both.
What the pipeline adds, and why
The pipeline extends the course tables in place, in an idempotent deploy step that only adds what's missing:
/* deploy 05 - extend the course tables for the pipeline (idempotent)
Dimension.Customer + SCD Type 2 columns: ValidFrom, ValidTo, IsCurrent, RowHash, LoadBatchID
Fact.Orders + SalesOrderNumber (business key for idempotent MERGE), SourceModifiedDate, LoadBatchID
Indexes that use the new columns are created with EXEC so this batch compiles before they exist. */
SET NOCOUNT ON;
IF COL_LENGTH(N'Dimension.Customer', N'ValidFrom') IS NULL
ALTER TABLE Dimension.Customer ADD
ValidFrom datetime2(3) NOT NULL CONSTRAINT DF_Customer_ValidFrom DEFAULT ('1900-01-01'),
ValidTo datetime2(3) NULL,
IsCurrent bit NOT NULL CONSTRAINT DF_Customer_IsCurrent DEFAULT (1),
RowHash varbinary(32) NULL,
LoadBatchID int NULL;
IF COL_LENGTH(N'Fact.Orders', N'SalesOrderNumber') IS NULL
ALTER TABLE Fact.Orders ADD
SalesOrderNumber nvarchar(25) NULL,
SourceModifiedDate datetime NULL,
LoadBatchID int NULL;
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_Customer_AlternateKey')
EXEC (N'CREATE NONCLUSTERED INDEX IX_Customer_AlternateKey
ON Dimension.Customer (CustomerAlternateKey, ValidFrom)
INCLUDE (ValidTo, IsCurrent, RowHash);');
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'UX_Orders_SalesOrderNumber')
EXEC (N'CREATE UNIQUE NONCLUSTERED INDEX UX_Orders_SalesOrderNumber
ON Fact.Orders (SalesOrderNumber) WHERE SalesOrderNumber IS NOT NULL;');
| Column / object | Purpose | Chapter |
|---|---|---|
ValidFrom, ValidTo, IsCurrent | Each customer version's validity window (SCD2) | 6 |
RowHash | SHA-256 of the Type 2 columns: one comparison finds a change | 6 |
SalesOrderNumber + unique filtered index | Business key, so re-loading an order updates instead of duplicating | 7 |
LoadBatchID, SourceModifiedDate | Lineage: which run wrote the row, and when the source changed it | 7, 12 |
Customer -1 "Unknown" | A fact whose customer is missing still loads, and the DQ gate counts it | 7, 10 |
dbo.SalesByYear | An aggregated indexed view (the course's view only pre-joins) | 8 |
Adding columns under an indexed view
dbo.OrdersByDate is created WITH SCHEMABINDING on Fact.Orders. Schema binding blocks changes to the columns the view uses, but adding new columns is allowed. That's why the deploy step can extend the course's fact table without dropping the view.
Key takeaways
- Write the grain first; it decides which dimensions and measures are possible.
- Surrogate keys are required as soon as a dimension keeps history.
SalesOrderNumberis a degenerate dimension and the fact's idempotency key.- The pipeline extends the course model with additive, idempotent DDL.