SQL Server system-versioned temporal tables provide a powerful way to keep data history for a table. It provides a way to query data as of a point in time, or the changes between a time period. Check the docs for more detail.
While history tables are great for many scenarios based off history data. When you need to calculate delta changes it gets a little more complicated as I am sure Temporal Tables were not designed for that purpose. In this case, delta means that over a period of time we want to get added records, changed records with calculation of differences, and deleted records.
To demonstrate how that works, here is a simplified example table with system versioning turned on:
1
2
3
4
5
6
7
8
9
10
11
12
13
CREATETABLE dbo.Orders (
Id bigint NOTNULLIdentity(1,1),
CustomerId bigint NOTNULL, --not really relevant for the example
TotalValue decimal(18,6) NOTNULL,
CONSTRAINT PK_Orders PRIMARYKEY CLUSTERED (Id),
)
ALTERTABLE dbo.Orders
ADD ValidFrom datetime2 GENERATED ALWAYS ASROWSTART HIDDEN NOTNULLCONSTRAINT DF_Orders_ValidFrom DEFAULT (SYSUTCDATETIME())
, ValidTo datetime2 GENERATED ALWAYS ASROWEND HIDDEN NOTNULLCONSTRAINT DF_Orders_ValidTo DEFAULT ('9999-12-31 23:59:59.9999999')
, PERIOD FOR SYSTEM_TIME ([ValidFrom], [ValidTo])
ALTERTABLE dbo.Orders SET (SYSTEM_VERSIONING =ON ( HISTORY_TABLE = [dbo].[OrderHistory], HISTORY_RETENTION_PERIOD =1Year ))
To get the wanted delta changes, we will need two dates as shown below. Note that both dates must be in UTC time zone as the ValidFrom and ValidTo columns will be saved in UTC too.
1
2
DECLARE@DeltaStart datetime2 -- for each run, this will be the value of @DeltaEnd of the previous run
DECLARE@DeltaEnd datetime2 = getutcdate() -- easier to put a limit so we know exactly what the next @DeltaStart will be
Finding new and updated records is fairly straight-forward:
1
2
3
4
5
6
7
8
-- 1. Query the current record using ValidFrom and ValidTo to detect changes
-- 2. Left join to that same record as it was at the @DeltaStart, if the record was changed, updated won't be null
-- 3. Get columns from the current record and calculate ValueDiff using current.TotalValue - updated.TotalValue
SELECTcurrent.Id, current.CustomerId, current.TotalValue, Status = IIF(IsNull(updated.Id, 0) =0, 'INSERTED', 'UPDATED')
, ValueDiff =current.TotalValue -IsNull(updated.TotalValue)
FROM dbo.Orders currentLEFTJOIN dbo.Orders for system_time asof@DeltaStart updated ON updated.Id =current.Id
wherecurrent.ValidFrom >@DeltaStart andcurrent.ValidTo <=@DeltaEnd
Deleted records will be removed from the main system-versioned table (Orders). However, they will remain in the history table (OrderHistory). Thus, we can use the history table as the start and if the same record is not found in current table, it was deleted:
1
2
3
4
5
6
7
-- 1. Look into history records that were closed between @DeltaStart and @DeltaEnd
-- 2. Left join with current record, if the current is null, the record was deleted
SELECT history.Id, history.CustomerId, history.TotalValue, Status ='DELETED' , ValueDiff =0- history.TotalValue
FROM dbo.OrderHistory history
LEFTJOIN dbo.Orders currentON updated.Id =current.Id
where ValidTo >@DeltaStart and ValidTo <=@DeltaEnd andcurrent.Id isnull
Let’s wrap this in a procedure to make it easier to consume:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
CREATEORALTERPROCEDURE spGetOrderDeltaChanges (@DeltaStart datetime2, @DeltaEnd datetime2)
ASBEGINSELECT current1.Id, current1.CustomerId, current1.TotalValue, [Status] = IIF(IsNull(updated1.Id, 0) =0, 'INSERTED', 'UPDATED')
, ValueDiff = current1.TotalValue -IsNull(updated1.TotalValue, 0)
FROM dbo.Orders current1
LEFTJOIN dbo.Orders for system_time asof@DeltaStart updated1 ON updated1.Id = current1.Id
WHERE current1.ValidFrom >@DeltaStart and current1.ValidFrom <=@DeltaEnd
UNIONSELECT history.Id, history.CustomerId, history.TotalValue, [Status] ='DELETED' , ValueDiff =0- history.TotalValue
FROM dbo.OrderHistory history
LEFTJOIN dbo.Orders current2 ON history.Id = current2.Id
where history.ValidTo >@DeltaStart and history.ValidTo <=@DeltaEnd and current2.Id isnullEND
Testing time! First, let’s add some records:
1
2
3
4
5
6
7
8
9
10
11
12
-- for the first run, we have no way to get @DeltaStart so we just use some time before the insert
DECLARE@DeltaStart datetime2 = dateadd(minute, -1, getutcdate())
INSERTINTO dbo.Orders (CustomerId, TotalValue)
values (1, 100)
, (2, 100)
DECLARE@DeltaEnd datetime2 = getutcdate()
exec spGetOrderDeltaChanges @DeltaStart, @DeltaEnd
-- don't forget to store the value of @DeltaEnd for the next run
Id
CustomerId
TotalValue
ChangeType
ValueDiff
1
1
100
INSERTED
100
2
2
100
INSERTED
100
This is self-explanatory, so let’s try to update record 1 and see the result:
1
2
3
4
5
6
7
8
9
DECLARE@DeltaStart datetime2 -- get the value from @DeltaEnd from the previous run
UPDATE dbo.Orders
SET TotalValue =200WHERE Id =1DECLARE@DeltaEnd datetime2 = getutcdate()
exec spGetOrderDeltaChanges @DeltaStart, @DeltaEnd
Id
CustomerId
TotalValue
ChangeType
ValueDiff
1
1
200
UPDATED
100
Note that record 2 was not touched so it won’t show on the delta results. Also, the ValueDiff column is only 100 as TotalValue was updated from 100 to 200. Now, let’s delete record 2 and see what happens:
1
2
3
4
5
6
7
8
DECLARE@DeltaStart datetime2 -- get the value from @DeltaEnd from the previous run
DELETE dbo.Orders
WHERE Id =2DECLARE@DeltaEnd datetime2 = getutcdate()
exec spGetOrderDeltaChanges @DeltaStart, @DeltaEnd
Id
CustomerId
TotalValue
ChangeType
ValueDiff
2
2
100
DELETED
-100
This time, we only show the deleted record. Note that the ValueDiff will be negative as the TotalValue changed from 100 to 0 (or nothing).
The use cases for this is limited, but though it was not explicitly designed for this, system-versioned tables can be used to calculate delta changes of a table’s data. The changes will accumulate from @DeltaStart to @DeltaEnd so however many transactions occur over this time period in dbo.Orders will be reported when the procedure is executed.