Following on from my previous post about modifying data in a temporal table, I will be looking into querying the data in this post.
When you want to get latest (actual) state of data in a temporal table, you can query the same way as you query a normal table. If the PERIOD columns are not hidden, their values will appear in a SELECT * query. If you specified PERIOD columns as hidden, their values won’t appear in a SELECT * query. When the PERIOD columns are hidden, reference the PERIOD columns specifically in the SELECT clause to return the values for these columns.
To perform any type of time-based analysis, use the new FOR SYSTEM_TIME clause with four temporal-specific sub-clauses to query data across the current and history tables.
- AS OF <date_time>
- FROM <start_date_time> TO <end_date_time>
- BETWEEN <start_date_time> AND <end_date_time>
- CONTAINED IN (<start_date_time> , <end_date_time>)
FOR SYSTEM_TIME can be specified independently for each table in a query. It can be used inside common table expressions, table-valued functions and stored procedures.
Using The AS OF Sub-Clause:
Use the AS OF sub-clause when you need to reconstruct state of data as it was at any specific time in the past. You can reconstruct the data with the precision of datetime2 datatype that was specified in the PERIOD column definitions.
The AS OF sub-clause clause can be used with constant literals or with variables, which allows you to dynamically specify time condition. The provided values are interpreted as UTC time.
This example returns the state of the dbo.Department table AS OF a specific date in the past.
/*State of entire table AS OF specific date in the past*/ SELECT [DeptID], [DeptName], [SysStartTime],[SysEndTime] FROM [dbo].[Department] FOR SYSTEM_TIME AS OF '2018-07-01 T10:00:00.7230011' ;
This example compares the values between two points in time for a subset of rows.
DECLARE @ADayAgo datetime2 SET @ADayAgo = DATEADD (day, -1, sysutcdatetime()) /*Comparison between two points in time for subset of rows*/ SELECT D_1_Ago.[DeptID], D.[DeptID], D_1_Ago.[DeptName], D.[DeptName], D_1_Ago.[SysStartTime], D.[SysStartTime], D_1_Ago.[SysEndTime], D.[SysEndTime] FROM [dbo].[Department] FOR SYSTEM_TIME AS OF @ADayAgo AS D_1_Ago JOIN [Department] AS D ON D_1_Ago.[DeptID] = [D].[DeptID] AND D_1_Ago.[DeptID] BETWEEN 1 and 5 ;
Using Views With AS-OF Sub-Clause:
Using views is very useful in scenarios when complex point-in time analysis is required. A common example is generating a business report today with the values for previous month. Usually, customers have a normalized database model which involves many tables with foreign key relationships.
Answering the question how data from that normalized model looked like at a point in the past can be very challenging, since all tables change independently. In this case, the best option is to create a view and apply the AS OF sub-clause to the entire view.
Using this approach allows you to decouple modeling of the data access layer from point-in time analysis as SQL Server will apply AS OF clause transparently to all temporal tables that participate in the view definition.
Furthermore, you can combine temporal with non-temporal tables in the same view and AS OF will be applied only to temporal ones. If the view does not reference at least one temporal table, applying temporal querying clauses to it will fail with an error.
/* Create view that joins three temporal tables: Department, CompanyLocation, LocationDepartments */ CREATE VIEW [dbo].[vw_GetOrgChart] AS SELECT [CompanyLocation].LocID , [CompanyLocation].LocName , [CompanyLocation].City , [Department].DeptID , [Department].DeptName FROM [dbo].[CompanyLocation] LEFT JOIN [dbo].[LocationDepartments] ON [CompanyLocation].LocID = LocationDepartments.LocID LEFT JOIN [dbo].[Department] ON LocationDepartments.DeptID = [Department].DeptID ; GO /* Querying view AS OF */ SELECT * FROM [vw_GetOrgChart] FOR SYSTEM_TIME AS OF '2018-09-01 T10:00:00.7230011' ;
Query For Changes To Specific Rows Over Time:
The temporal sub-clauses FROM…TO, BETWEEN…AND and CONTAINED IN are useful when you want to perform a data audit.
The first two sub-clauses return row versions that overlap with a specified period (i.e. those that started before given period and ended after it), while CONTAINED IN returns only those that existed within the specified period boundaries.
If you search for non-current row versions only, I recommend you use CONTAINED IN as it works only with the history table and will yield the best query performance. Use ALL when you need to query current and historical data without any restrictions.
/* Query using BETWEEN...AND sub-clause*/ SELECT [DeptID] , [DeptName] , [SysStartTime] , [SysEndTime] , IIF (YEAR(SysEndTime) = 9999, 1, 0) AS IsActual FROM [dbo].[Department] FOR SYSTEM_TIME BETWEEN '2018-01-01' AND '2018-12-31' WHERE DeptId = 1 ORDER BY SysStartTime DESC; /* Query using CONTAINED IN sub-clause */ SELECT [DeptID], [DeptName], [SysStartTime],[SysEndTime] FROM [dbo].[Department] FOR SYSTEM_TIME CONTAINED IN ('2018-04-01', '2018-09-25') WHERE DeptId = 1 ORDER BY SysStartTime DESC ; /* Query using ALL sub-clause */ SELECT [DeptID] , [DeptName] , [SysStartTime] , [SysEndTime] , IIF (YEAR(SysEndTime) = 9999, 1, 0) AS IsActual FROM [dbo].[Department] FOR SYSTEM_TIME ALL ORDER BY [DeptID], [SysStartTime] Desc
Hope you have enjoyed the 3 part series on Temporal Tables.