Business Professionals
Power BI | Power Pivot | Power Query | DAX
Cloud Flows | RPA | AI Builder | Copilot
60+ Formulas | Data Stories | Advanced Reporting & Modeling
VB Programming | Report Automation |
MS-Office Automation
Techno-Business Professionals
Power BI | Power Query | Advanced DAX | SQL - Query &
Programming
Microsoft Fabric | Power BI | Power Query | Advanced DAX |
SQL - Query & Programming
Power BI | Power Apps | Power Automate | Copilot Studio | Power Pages | Dataverse
Microsoft Power Apps | Microsoft Power Automate
Power BI | Adv. DAX | SQL (Query & Programming) |
VBA | Python | Web Scrapping | API Integration
Power BI | Power Apps | Power Automate |
SQL (Query & Programming)
Power BI | Adv. DAX | Power Apps | Power Automate |
SQL (Query & Programming) | VBA | Python | Web Scrapping | API Integration
Power Apps | Power Automate | SQL | VBA | Python |
Web Scraping | RPA | API Integration
Technology Professionals
Power BI | DAX | SQL | ETL with SSIS | SSAS | VBA | Python
Power BI | SQL | Azure Data Lake | Synapse Analytics |
Data Factory | Databricks | Power Apps | Power Automate |
Azure Analysis Services
Microsoft Fabric | Power BI | SQL | Lakehouse |
Data Factory (Pipelines) | Dataflows Gen2 | KQL | Delta Tables | Power Apps | Power Automate
Power BI | Power Apps | Power Automate | SQL | VBA | Python | API Integration
Power BI | Advanced DAX | Databricks | SQL | Lakehouse Architecture
Business Professionals
Power BI | Power Pivot | Power Query | DAX
Cloud Flows | RPA | AI Builder | Copilot
60+ Formulas | Data Stories | Advanced Reporting & Modeling
VB Programming | Report Automation |
MS-Office Automation
Techno-Business Professionals
Power BI | Power Query | Advanced DAX | SQL - Query &
Programming
Microsoft Fabric | Power BI | Power Query | Advanced DAX |
SQL - Query & Programming
Power BI | Power Apps | Power Automate | Copilot Studio | Power Pages | Dataverse
Microsoft Power Apps | Microsoft Power Automate
Power BI | Adv. DAX | SQL (Query & Programming) |
VBA | Web Scrapping | API Integration
Power BI | Power Apps | Power Automate |
SQL (Query & Programming)
Power BI | Adv. DAX | Power Apps | Power Automate |
SQL (Query & Programming) | VBA | Web Scrapping | API Integration
Power Apps | Power Automate | SQL | VBA |
Web Scraping | RPA | API Integration
Technology Professionals
Power BI | DAX | SQL | ETL with SSIS | SSAS | VBA
Power BI | SQL | Azure Data Lake | Synapse Analytics |
Data Factory | Azure Analysis Services
Microsoft Fabric | Power BI | SQL | Lakehouse |
Data Factory (Pipelines) | Dataflows Gen2 | KQL | Delta Tables
Power BI | Power Apps | Power Automate | SQL | VBA | API Integration
Power BI | Advanced DAX | Databricks | SQL | Lakehouse Architecture
Your finance dashboard shows a customer’s revenue trend dropping sharply last quarter, but the business owner swears their discount agreement changed mid-period and the report isn’t reflecting it correctly. You dig into the warehouse and realise you have no reliable, queryable history of how the customer record changed over time.
This article walks through practical patterns for using MS-SQL system-versioned temporal tables alongside Slowly Changing Dimensions (SCD) to build auditable history for analytics systems, using one realistic scenario end-to-end.
You’re maintaining a SQL Server-based analytics platform feeding Power BI and Excel reports. Core entities like Customer, Product, and PriceAgreement live in a data warehouse on SQL Server.
The pain:
dimCustomer table is type-1 only: updates overwrite history.You want:
We’ll focus on Customer as the core dimension and connect three layers:
dbo.Customer in an OLTP-ish database.dw.dimCustomer with SCD Type 2 behaviour.System-versioned temporal tables in SQL Server keep a full history of row versions, managed by the engine.
Key properties (current SQL Server behaviour):
ValidFrom (period start)ValidTo (period end)PERIOD FOR SYSTEM_TIME is declared over those columns.UPDATE or DELETE:
ValidFrom/ValidTo are automatically maintained by SQL Server.FOR SYSTEM_TIME AS OF @t – snapshot at a point in time.FROM ... TO, BETWEEN ... AND, CONTAINED IN – intervals.Important constraints:
datetime2 for the period columns.For our scenario, we’ll enable system versioning on the operational Customer table to get a reliable change log with minimal application changes.
Customer into a Temporal TableAssume you currently have:
CREATE TABLE dbo.Customer
(
CustomerID int NOT NULL PRIMARY KEY,
CustomerCode nvarchar(50) NOT NULL,
Name nvarchar(200) NOT NULL,
DiscountPercent decimal(5,2) NOT NULL,
AccountManager nvarchar(100) NULL,
IsActive bit NOT NULL DEFAULT (1),
-- no history columns yet
);
You want to convert this into a system-versioned temporal table without breaking existing code.
We add ValidFrom and ValidTo with defaults, then declare the period:
ALTER TABLE dbo.Customer
ADD ValidFrom datetime2(7) GENERATED ALWAYS AS ROW START HIDDEN
CONSTRAINT DF_Customer_ValidFrom DEFAULT SYSUTCDATETIME(),
ValidTo datetime2(7) GENERATED ALWAYS AS ROW END HIDDEN
CONSTRAINT DF_Customer_ValidTo DEFAULT CONVERT(datetime2(7), '9999-12-31 23:59:59.9999999'),
PERIOD FOR SYSTEM_TIME (ValidFrom, ValidTo);
Notes:
GENERATED ALWAYS AS ROW START/END ensures SQL Server manages these values.HIDDEN keeps existing SELECT * queries from suddenly returning extra columns, which is often important in legacy code.ValidTo is a common pattern; SQL Server will adjust it automatically when rows are updated.You can let SQL Server create the history table automatically, or specify your own. Explicit is usually better:
CREATE TABLE dbo.CustomerHistory
(
CustomerID int NOT NULL,
CustomerCode nvarchar(50) NOT NULL,
Name nvarchar(200) NOT NULL,
DiscountPercent decimal(5,2) NOT NULL,
AccountManager nvarchar(100) NULL,
IsActive bit NOT NULL,
ValidFrom datetime2(7) NOT NULL,
ValidTo datetime2(7) NOT NULL
);
ALTER TABLE dbo.Customer
SET (SYSTEM_VERSIONING = ON
(HISTORY_TABLE = dbo.CustomerHistory,
DATA_CONSISTENCY_CHECK = ON));
DATA_CONSISTENCY_CHECK = ON makes SQL Server validate that period values don’t overlap for the same primary key.
At this point:
UPDATE and DELETE operations on dbo.Customer will write old versions into dbo.CustomerHistory.FOR SYSTEM_TIME AS OF.Example: what was the discount for CustomerID = 101 as of a specific date?
DECLARE @AsOf datetime2(7) = '2026-09-30T23:59:59.0000000';
SELECT CustomerID,
CustomerCode,
Name,
DiscountPercent,
AccountManager,
IsActive
FROM dbo.Customer
FOR SYSTEM_TIME AS OF @AsOf
WHERE CustomerID = 101;
This query transparently reads from the current and history tables to return the correct version for that timestamp.
Temporal tables give you operational history, but analytics systems usually want SCD Type 2 dimensions with:
ValidFrom / ValidTo ranges and an IsCurrent flag.We’ll build dw.dimCustomer as a Type 2 dimension sourced from dbo.Customer’s temporal history.
CREATE TABLE dw.dimCustomer
(
CustomerSK int IDENTITY(1,1) NOT NULL PRIMARY KEY,
CustomerID int NOT NULL, -- business key
CustomerCode nvarchar(50) NOT NULL,
Name nvarchar(200) NOT NULL,
DiscountPercent decimal(5,2) NOT NULL,
AccountManager nvarchar(100) NULL,
IsActive bit NOT NULL,
ValidFrom datetime2(7) NOT NULL,
ValidTo datetime2(7) NOT NULL,
IsCurrent bit NOT NULL,
CONSTRAINT UQ_dimCustomer_BK_Period
UNIQUE (CustomerID, ValidFrom)
);
We’ll populate this from the temporal source by materialising all versions.
For the first full load, we can flatten the temporal history into SCD rows.
INSERT INTO dw.dimCustomer
(CustomerID, CustomerCode, Name, DiscountPercent,
AccountManager, IsActive,
ValidFrom, ValidTo, IsCurrent)
SELECT c.CustomerID,
c.CustomerCode,
c.Name,
c.DiscountPercent,
c.AccountManager,
c.IsActive,
c.ValidFrom,
c.ValidTo,
CASE WHEN c.ValidTo = CONVERT(datetime2(7), '9999-12-31 23:59:59.9999999')
THEN 1 ELSE 0 END AS IsCurrent
FROM dbo.Customer
FOR SYSTEM_TIME ALL AS c;
FOR SYSTEM_TIME ALL returns both current and historical versions. We use the far-future ValidTo to mark current rows.
This gives us a dimension row for every historical version of each customer.
For daily or hourly ETL, you don’t want to reload everything. Instead, you want only rows that changed since the last ETL run.
One pragmatic pattern:
FOR SYSTEM_TIME FROM @LastRun TO @ThisRun to get changed rows.Control table:
CREATE TABLE dw.ETLRunLog
(
RunID int IDENTITY(1,1) PRIMARY KEY,
ProcessName nvarchar(100) NOT NULL,
StartedAt datetime2(7) NOT NULL,
CompletedAt datetime2(7) NULL
);
Incremental load procedure (simplified):
CREATE OR ALTER PROCEDURE dw.LoadDimCustomerIncremental
AS
BEGIN
SET NOCOUNT ON;
DECLARE @LastCompleted datetime2(7);
DECLARE @ThisRunStart datetime2(7) = SYSUTCDATETIME();
SELECT @LastCompleted = MAX(CompletedAt)
FROM dw.ETLRunLog
WHERE ProcessName = N'LoadDimCustomerIncremental'
AND CompletedAt IS NOT NULL;
IF @LastCompleted IS NULL
BEGIN
-- No previous run: fall back to full load
-- In practice, call the full-load procedure here.
RETURN;
END;
-- Get all versions that changed in the window
;WITH Changed AS
(
SELECT c.CustomerID,
c.CustomerCode,
c.Name,
c.DiscountPercent,
c.AccountManager,
c.IsActive,
c.ValidFrom,
c.ValidTo
FROM dbo.Customer
FOR SYSTEM_TIME FROM @LastCompleted TO @ThisRunStart AS c
)
-- Upsert into SCD dimension
MERGE dw.dimCustomer AS target
USING Changed AS src
ON target.CustomerID = src.CustomerID
AND target.ValidFrom = src.ValidFrom
WHEN MATCHED THEN
UPDATE SET
target.CustomerCode = src.CustomerCode,
target.Name = src.Name,
target.DiscountPercent = src.DiscountPercent,
target.AccountManager = src.AccountManager,
target.IsActive = src.IsActive,
target.ValidTo = src.ValidTo,
target.IsCurrent = CASE WHEN src.ValidTo = CONVERT(datetime2(7), '9999-12-31 23:59:59.9999999')
THEN 1 ELSE 0 END
WHEN NOT MATCHED BY TARGET THEN
INSERT (CustomerID, CustomerCode, Name, DiscountPercent,
AccountManager, IsActive,
ValidFrom, ValidTo, IsCurrent)
VALUES (src.CustomerID, src.CustomerCode, src.Name, src.DiscountPercent,
src.AccountManager, src.IsActive,
src.ValidFrom, src.ValidTo,
CASE WHEN src.ValidTo = CONVERT(datetime2(7), '9999-12-31 23:59:59.9999999')
THEN 1 ELSE 0 END);
INSERT INTO dw.ETLRunLog (ProcessName, StartedAt, CompletedAt)
VALUES (N'LoadDimCustomerIncremental', @ThisRunStart, SYSUTCDATETIME());
END;
This pattern:
You still need to think carefully about late-arriving facts and how your fact-load logic chooses dimension versions (usually based on event date vs. ValidFrom/ValidTo). But the dimension itself is now auditable and complete.
You now have two layers of history:
dbo.Customer / dbo.CustomerHistory.dw.dimCustomer.They serve different purposes.
Practical pattern for our scenario:
Customer, PriceAgreement, etc.).Temporal tables track row versions, not:
If you need user attribution:
With SCD Type 2, your fact load must pick the correct version based on event date.
Example pattern for a sales fact load:
INSERT INTO dw.factSales
(SaleDate, CustomerSK, Amount)
SELECT f.SaleDate,
dc.CustomerSK,
f.Amount
FROM staging.Sales AS f
JOIN dw.dimCustomer AS dc
ON dc.CustomerID = f.CustomerID
AND f.SaleDate >= dc.ValidFrom
AND f.SaleDate < dc.ValidTo;
This ensures each sale points to the customer version that was valid on SaleDate.
Temporal history tables can grow quickly. Over-indexing them:
Practical approach:
Example minimal index:
CREATE CLUSTERED INDEX CX_CustomerHistory_PK_Period
ON dbo.CustomerHistory (CustomerID, ValidFrom);
When you disable system versioning:
ALTER TABLE dbo.Customer
SET (SYSTEM_VERSIONING = OFF);
If you must turn it off:
For analytics systems that need auditable history, MS-SQL temporal tables and Slowly Changing Dimensions work best as a layered pattern:
If your reports are currently showing “latest-only” values and you’re losing historical context, starting with temporal tables on your core entities and building SCD dimensions on top is a concrete, implementable way to restore trust in your numbers.
This article reflects a shift toward using SQL Server temporal tables as the authoritative change log feeding Slowly Changing Dimensions, a pattern that matters most for teams running auditable analytics on frequently changing master data.
Professionals who want to apply these patterns to their own data can explore Excelgoodies' Power BI With SQL programme - taught live by instructors, with certification awarded once a real project is running at work.
Insights compiled through ongoing industry research and discussions within the Excelgoodies Analytics Community.
MS-SQL
New
Next Batches Now Live
Power BI
SQL
Power Apps
Power Automate
Microsoft Fabrics
Azure Data Engineering