Options to Retrieve SQL Server Temporal Table and History Data

By:   |   Comments (7)   |   Related: > Temporal Tables


Problem

In a previous tip, we discussed how SQL Server temporal tables work and also how to query them, but we would like to understand more about how to use the new clause FOR SYSTEM_TIME and how the new subclauses work in SELECT … FROM type queries to get varying versions of the rows or a complete picture of the data at a particular point in time.

Solution

In SQL Server 2016, we have a new clause FOR SYSTEM_TIME in the SELECT ... FROM statement, which has five new subclauses to query temporal table data:

  1. AS OF <datetime>
  2. ALL
  3. FROM <start_datetime> TO <end_datetime>
  4. BETWEEN <start_datetime> AND <end_datetime>
  5. CONTAINED IN (start_datetime, end_datetime)

Let’s dive into an example to understand how these subclauses work and what type of data they return and why. We will assume a real-life scenario where there are already manual solutions in place to keep track of history in some fashion. We have a Volunteers table which keeps track of each volunteer’s current duty assignment. We also have a VolunteersHistory table which keeps track of history changes for the Volunteers table. We are going to bring both tables together into a temporal solution which automatically keeps track of history. Please read this for complete requirements to add an existing history table to a temporal table.

Create Temporal Table Example

The following code will create a database, some tables and insert some sample data.  We will also enable the temporal features on the tables.

-- Create a database TemporalDB for our example
			
USE master 
GO 

IF DB_ID('TemporalDB') IS NOT NULL DROP DATABASE TemporalDB 
CREATE DATABASE TemporalDB 
GO 

USE TemporalDB; 
GO 
  
-- Volunteers table
CREATE TABLE dbo.Volunteers
(
  id INT NOT NULL CONSTRAINT PK_Volunteers PRIMARY KEY NONCLUSTERED,
  Serving_Area VARCHAR(20) NULL,
  Volunteer_name VARCHAR(25) NOT NULL,
  sysstart DATETIME2(0) NOT NULL,
  sysend DATETIME2(0) NOT NULL
);
CREATE UNIQUE CLUSTERED INDEX  ix_Volunteers ON dbo.Volunteers  (id, sysstart, sysend);
 
-- Insert data into Volunteers table 
INSERT INTO dbo.Volunteers (id, Serving_Area, Volunteer_name, sysstart, sysend)
VALUES
   (1 , NULL,           'David', '2018-01-31 17:44:04', '9999-12-31 23:59:59'), 
   (2 , 'Nursing Home', 'Eliza', '2018-01-31 17:44:04', '9999-12-31 23:59:59'), 
   (3 , 'Nursing Home', 'Inara', '2018-01-31 17:44:04', '9999-12-31 23:59:59'), 
   (4 , 'Shelter',      'Sam',   '2018-01-31 17:44:04', '9999-12-31 23:59:59'), 
   (5 , 'Shelter',      'Leo',   '2018-02-01 19:54:20', '9999-12-31 23:59:59'), 
   (6 , 'Baby Sitting', 'Steve', '2018-03-29 18:44:04', '9999-12-31 23:59:59'), 
   (7 , 'Soup Kitchen', 'Aaron', '2018-03-01 17:44:04', '9999-12-31 23:59:59'), 
   (8 , 'School',       'Laila', '2018-03-01 17:44:04', '9999-12-31 23:59:59'), 
   (9 , 'Soup Kitchen', 'Eva',   '2018-03-01 17:44:04', '9999-12-31 23:59:59'), 
   (10, 'School',       'Sean',  '2018-03-29 17:44:04', '9999-12-31 23:59:59'),  
   (11, 'Library',      'Uriel', '2018-03-29 18:44:04', '9999-12-31 23:59:59'); 
 
-- VolunteersHistory table
CREATE TABLE dbo.VolunteersHistory
( id INT NOT NULL,
  Serving_Area VARCHAR(20) NULL,
  Volunteer_name VARCHAR(25) NOT NULL,
  sysstart DATETIME2(0) NOT NULL,
  sysend DATETIME2(0) NOT NULL );
CREATE CLUSTERED INDEX ix_VolunteersHistory
  ON dbo.VolunteersHistory(id, sysstart, sysend) WITH (DATA_COMPRESSION = PAGE);
 
-- Insert some historical data into VolunteersHistory table
INSERT INTO dbo.VolunteersHistory  (id, Serving_Area, Volunteer_name, sysstart, sysend) 
VALUES
   (6 , 'Shelter',      'Steve',   '2018-01-31 17:44:04', '2018-03-29 18:44:04'), 
   (7 , 'Baby Sitting', 'Aaron',   '2018-01-31 17:44:04', '2018-03-01 17:44:04'), 
   (9 , 'Lost Found',   'Eva',     '2018-01-31 17:44:04', '2018-01-31 18:44:04'),
   (9 , 'Baby Sitting', 'Eva',     '2018-01-31 18:45:04', '2018-03-01 17:44:04'), 
   (11, 'Lost Found',   'Uriel',   '2018-01-31 17:44:04', '2018-01-31 18:44:04'), 
   (11, 'Baby Sitting', 'Uriel',   '2018-01-31 18:44:04', '2018-03-29 18:44:04'), 
   (12, 'Traffic',      'Emily',   '2018-01-31 17:44:04', '2018-03-29 19:01:41'), 
   (13, 'Traffic',      'Michael', '2018-01-31 17:44:04', '2018-01-31 18:44:04'), 
   (14, 'Traffic',      'Tom',     '2018-01-31 17:44:04', '2018-01-31 18:44:04'); 
 
-- Enable system versioning
ALTER TABLE dbo.Volunteers ADD
  PERIOD FOR SYSTEM_TIME (sysstart, sysend);
 
ALTER TABLE dbo.Volunteers
  SET ( SYSTEM_VERSIONING = ON ( HISTORY_TABLE = dbo.VolunteersHistory ) );
 
-- Now query tables to look at the data
SELECT * FROM dbo.Volunteers;
SELECT * FROM dbo.VolunteersHistory;
temporal table query results

Let’s say we want to query the above data using the new subclauses that came out with temporal tables in SQL Server 2016. We mentioned in the beginning that there are 5 subclauses of FOR SYSTEM_TIME clause. Now we are going to look at them one by one and see the difference in their results. This analysis will give you a good idea where you will use each subclause based on your business requirement.

Querying Temporal Table Data Using SYSTEM_TIME AS OF and ALL

SYSTEM_TIME AS OF

The AS OF subclause returns rows from the temporal and history table that are valid up to the time you specify. It gives you the complete snapshot of the current values until the specified time.

DECLARE @datetime AS DATETIME2(0) = '2018-03-01 17:44:04';
			
SELECT * FROM dbo.Volunteers FOR SYSTEM_TIME AS OF @datetime ORDER BY sysend DESC; 

Results:

temporal table query results

Notice the result of AS OF query above:

  • The first 5 rows are the same as they are in the Volunteers table because they did not have any changes.
  • The rest of the 6 rows were changed (updated or deleted) and it shows their current values up until the specified time in the query.
  • The last 3 rows are records that are no longer active as evident from the column sysend. Also, id (12) does not exist anymore in the Volunteers table, but only in the VolunteersHistory table.

SYSTEM_TIME ALL

On the other hand, ALL gives you everything from the current and history table. For our example, the Volunteers table has 11 rows and VolunteersHistory table has 9 rows, so ALL will give you all 20 rows.

DECLARE @datetime AS DATETIME2(0) = '2018-03-01 17:44:04';
  
SELECT * FROM dbo.Volunteers FOR SYSTEM_TIME ALL ORDER BY sysend DESC;-- ALL 
GO

The above query is equivalent to the following UNION ALL query, but look at how much simpler and cleaner the temporal query is.

DECLARE @datetime AS DATETIME2(0) = '2018-03-01 17:44:04';
 
SELECT * FROM dbo.Volunteers WHERE sysstart <= @datetime AND sysend > @datetime  
UNION ALL
SELECT * FROM dbo.VolunteersHistory WHERE sysstart <= @datetime AND sysend > @datetime;
GO	
	

Results:

temporal table query results

What if you just want to see the data for one particular volunteer?

DECLARE @datetime AS DATETIME2(0) = '2018-03-01 17:44:04', @id AS INT = 9;

SELECT * FROM dbo.Volunteers FOR SYSTEM_TIME AS OF @datetime WHERE id = @id	

Here Eva’s current assignment is Soup Kitchen. Now try with time 1 second before the above one. We will see that Eva’s volunteering assignment at that time was Baby Sitting.

DECLARE @datetime AS DATETIME2(0) = '2018-03-01 17:44:03', @id AS INT = 9;

SELECT * FROM dbo.Volunteers FOR SYSTEM_TIME AS OF @datetime WHERE id = @id;
temporal table query results

The value of Baby Sitting was only valid during the time shown above in the red circle. Here sysstart time is before the specified time in the query.

Querying Temporal Table Data Using SYSTEM_TIME Using FROM and TO clause

The subclause FROM-TO returns results by combing the temporal and history table, but we don’t see the current assignment of Eva because this clause excludes the upper boundary of the end time.

DECLARE
  @start AS DATETIME2(0)= '2018-01-31 17:44:04', -- time of Eva's first assignment
  @end   AS DATETIME2(0)= '2018-03-01 17:44:04', -- time of Eva's current assignment
  @id AS INT = 9;
 
SELECT * FROM dbo.Volunteers FOR SYSTEM_TIME FROM @start TO @end WHERE id = @id;
 
-- Equivalent Union Query
SELECT * FROM dbo.Volunteers WHERE id = @id AND sysstart < @end AND sysend > @start
UNION ALL
SELECT * FROM dbo.VolunteersHistory WHERE id = @id AND sysstart < @end   AND sysend > @start;
GO

Results:

temporal table query results

Querying Temporal Table Data Using SYSTEM_TIME Using BETWEEN AND pair

The subclause BETWEEN-AND returns combined results from both the temporal and history tables and we can see the current assignment of Eva. This is because this subclause includes the upper boundary of the end time.

DECLARE
  @start AS DATETIME2(0)= '2018-01-31 17:44:04', -- time of Eva's first assignment
  @end   AS DATETIME2(0)= '2018-03-01 17:44:04',  -- time of Eva's current assignment
  @id AS INT = 9;
 
SELECT * FROM dbo.Volunteers FOR SYSTEM_TIME BETWEEN @start AND @end WHERE id = @id;
 
-- UNION ALL equivalent
SELECT * FROM dbo.Volunteers WHERE id = @id AND sysstart < = @end AND sysend > @start
UNION ALL
SELECT * FROM dbo.VolunteersHistory WHERE id = @id AND sysstart < = @end AND sysend > @start;
GO

Results:

temporal table query results

Querying Temporal Table Data Using SYSTEM_TIME Using Contained IN

This subclause gives all rows from the history table included in the range specified. As you will observe in the result, Eva’s current assignment is missing. This is because CONTAINED IN only returns data from the history table.

DECLARE
  @start AS DATETIME2(0)= '2018-01-31 17:44:04', -- time of Eva's first assignment
  @end AS DATETIME2(0)= '2018-03-01 17:44:04', -- time of Eva's current assignment
  @id AS INT = 9;
 
SELECT * FROM dbo.Volunteers FOR SYSTEM_TIME CONTAINED IN ( @start , @end ) WHERE id = @id;
 
-- Equivalent UNION ALL query
SELECT * FROM dbo.Volunteers WHERE id = @id AND sysstart > = @start AND sysend < = @end
UNION ALL
SELECT * FROM dbo.VolunteersHistory WHERE id = @id AND sysstart > = @start AND sysend < = @end
GO

Results:

temporal table query results

Comparing Temporal Table SYSTEM _TIME Query Results

Now we will run 3 of the subclause queries together and will see their results in one screenshot which will allow us to compare them easily. Discussion follows the screenshots.

Combined Result

temporal table query results

'2018-01-31 17:44:04' is the time of Eva's first assignment and '2018-03-01 17:44:04' is the time of Eva's current (latest) assignment. Pay attention to the 3 @end values in the queries above.

The subclause FROM-TO returns a result combing the temporal and history table, but we don’t see the current assignment of Eva because this clause excludes the upper boundary of the end time.

The subclause BETWEEN-AND returns a combined result from both the temporal and history tables and we can see the current assignment of Eva. This is because this subclause includes the upper boundary of the end time.

The CONTAINED-IN clause returns results only from the history table and includes both the upper and lower boundaries of times.

Now let us look at the same queries, but instead of using the exact time at which Eva’s current assignment was given to her, we will add 1 second of time, so the end time in this case will be ‘2018-03-01 17:44:05’.

temporal db

The subclause FROM-TO query now returns 3 rows because now the time of the current assignment is under the upper boundary of the end time.

The subclause BETWEEN-AND query returns the same 3 rows as before because during this additional one second, Eva’s assignment was not changed. 

The CONTAINED-IN query returns the same results as before because it only returns data from the history table.

Summary

In this article I tried to cover the FOR SYSTEM_TIME clause with examples and discussions. We are now familiar with the 5 subclauses and how they behave in a query. I tried to show them one by one and also combined together so that you can see the differences and similarities in their results.  I have also shown equivalent UNION ALL queries to prove the beauty of Temporal tables. Your code will be much simpler and cleaner when you use these subclauses that came with temporal tables in SQL Server 2016. This system time-based analysis will give you a good idea where you will use each subclause based on your business requirements.

Next Steps
  • You can use the example of code in this article and play with it to gain deeper understanding.
  • Please read more about temporal tables on MSSQLTips.
  • To read more about temporal table querying, read this.


sql server categories

sql server webinars

subscribe to mssqltips

sql server tutorials

sql server white papers

next tip



About the author
MSSQLTips author Ameena Lalani Ameena Lalani is a MCSA on SQL Server 2016. She is a SQL Server veteran and started her journey with SQL Server 2000. She has implemented numerous High Availability and Disaster Recovery solutions.

This author pledges the content of this article is based on professional experience and not AI generated.

View all my tips



Comments For This Article




Wednesday, April 14, 2021 - 3:27:37 AM - Andreas Kleinbichler Back To Top (88528)
Hi Ameena !
Great article and it helped me a lot.

But the query for Contained In:

-- Equivalent UNION ALL query
SELECT * FROM dbo.Volunteers WHERE id = @id AND sysstart > = @start AND sysend < = @end
UNION ALL
SELECT * FROM dbo.VolunteersHistory WHERE id = @id AND sysstart > = @start AND sysend < = @end
GO

Should be changed to:
SELECT * FROM dbo.VolunteersHistory WHERE id = @id AND sysstart > = @start AND sysend < = @end

as the temporal clause contains only records from the history table

br Andreas

Wednesday, January 9, 2019 - 7:58:56 AM - Mykhaylo Tytarenko Back To Top (78680)

Hi Ameena.

Wrong script inside SYSTEM_TIME ALL caption.

Result of second script (UNION ALL query) is not equal for script above.

Thanks.


Wednesday, January 2, 2019 - 12:20:29 PM - Ameena Lalani Back To Top (78603)

You can do that but I would recommend using some other unique id column along with System time to make sure you are getting the latest value. The reason for this is, in highly transactional systems, two rows possibly could have the same timestamp with different values. I am giving example from one of the other articles I have written on recovering data from Temporal table https://www.mssqltips.com/sqlservertip/5109/benefits-of-using-sql-server-temporal-tables--part-1/.

INSERT INTO dbo.Customer(CustomerId, FirstName, LastName, Amount_purchased)
SELECT CustomerId, FirstName,LastName, Amount_purchased FROM dbo.Customer FOR SYSTEM_TIME AS OF '2018-04-19 18:16:43.3351187'
WHERE CustomerId =2

You can also check CDC (Change Data Capture) functionality as an alternative to the temporal table and see if that is more suitable in your situation. Without knowing all other variables, I cannot suggest one method over the other. You have to make this informed decision. I hope, this answers all your questions.

Best of Luck!


Tuesday, January 1, 2019 - 6:33:31 PM - Shimmy Back To Top (78593)

Hi Ameena and thanks for your reply!

I don't mind using anything else as long as my goal is achieved, and my goal is to be able to roll back to previous versions.

I thought about using temporal tables and search only the history teable for results ordered by end-time ascending, and use the top result as the latest version (if any), isn't that how I can achieve my requirement?

Are there better alternatives for me than Temporal Tables?


Tuesday, January 1, 2019 - 2:29:50 PM - Ameena Lalani Back To Top (78590)

 Hi Shimmy,

Currently, there is no support for versioning in Temporal Tables. The data in history table is stored in with the physical system time stamp. It does not know the logic of your business. Hence there is no support for Application time. It is best explained in another article written by my peer at MSSQLTips. I hope it helps you.

https://www.mssqltips.com/sqlservertip/3680/introduction-to-sql-server-temporal-tables/


Tuesday, January 1, 2019 - 1:17:03 AM - Shimmy Back To Top (78588)

Is there a way to treat Temporal Tables as version data, meaning that it will only return latest version of searched row, that is other than the current one, or return nothing?


Monday, December 31, 2018 - 7:12:27 PM - Shimmy Back To Top (78587)

Very informative, thank you!















get free sql tips
agree to terms