Guide to Using Temporal Tables in SQL 2016
/Temporal tables give us the ability to add version history to our data. If you want to track data changes to a table, see what a value was at a specific point in time, find when a value was last changed, or union historic records with current table records, temporal tables can do all of that. Temporal tables can be created on new or existing tables. Temporal tables are an exact replica of a new or existing table. You can think of temporal tables as a versioning table with the same definition as the table it's tracking, they are paired. This sounds more complicated than it is, let's get to the scripts.
First we're going to create a new table that also includes our temporal table.
CREATE TABLE dbo.Orders ( Id int IDENTITY (1,1) PRIMARY KEY CLUSTERED ,OrderNumber int NOT NULL ,ProductID int NOT NULL ,SalesPersonID int NOT NULL ,Amount decimal (7,2) NOT NULL ,SalesDate datetime2(0) DEFAULT GETDATE() ,ValidFrom datetime2(2) GENERATED ALWAYS AS ROW START -- Required field, can be named whatever you like. ,Validto datetime2(2) GENERATED ALWAYS AS ROW END -- Required field, can be named whatever you like. ,PERIOD FOR SYSTEM_TIME (ValidFrom, ValidTo) ) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = dbo.Orders_Archive));
Let's look at what's new in our table definition. See the ValidFrom and ValidTo columns, these are referred to as period columns and are used to track the state of the row at the time the row was updated, the data type here should be datetime2(2), these columns are required. The GENERATED ALWAYS (START | END) arguments specify a start and end time for when the record was valid. The PERIOD FOR SYSTEM_TIME argument specifies the names of the columns the system will use to record the valid period of a row of data, in this case ValidfFrom and ValidTo. Finally we have the table option WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = dbo.Orders_Archive)). SYSTEM_VERSIONING just enables versioning (for the temporal table) on the table you are creating. HISTORY_TABLE lets us target a specific history table, this could be an existing history table or a new history table. The above script will create a new history table named dbo.Orderes_Archive.
Now that we have our temporal table setup, let's see how it works. First we need to insert some data, notice we do not specify values for the ValidFrom and ValidTo columns.
INSERT INTO dbo.Orders (OrderNumber, ProductID, SalesPersonID, Amount) SELECT 85462, 989, 11, 10.99 INSERT INTO dbo.Orders (OrderNumber, ProductID, SalesPersonID, Amount) SELECT 85486, 325, 12, 14.95 INSERT INTO dbo.Orders (OrderNumber, ProductID, SalesPersonID, Amount) SELECT 85501, 193, 11, 21.99
Now let's see what's in the tables we just created. We won't have anything in the history table Orders_Archive, not until we actually modify data in the Orders table. Query the tables, then run the update, and then query both tables again to see the results.
SELECT * FROM dbo.Orders; SELECT * FROM dbo.Orders_Archive; --Now we need to update the Orders table. UPDATE dbo.Orders SET Amount = 50.00 WHERE Id = 2;
After the update we should now have a single row in the Orders_Archive history table. This row represents the data before it was modified and the period of time that is was valid using the ValidFrom and ValidTo values.
Now what if we want to query the Orders table and this time include the table history values. We can do this using the SYSTEM_TIME clause in our FROM. Here we want to return all values that were valid between yesterday and tomorrow from both the parent and history table. Notice we don't have to call the Orders_Archive table directly, using SYSTEM_TIME allows us to query both tables by referencing only the parent Orders table.
DECLARE @Start datetime2(2) = DATEADD(DD, -1, GETDATE()) ,@End datetime2(2) = DATEADD(DD, 1, GETDATE()) SELECT * FROM dbo.Orders FOR SYSTEM_TIME BETWEEN @Start AND @End
There are 4 temporal-specific sub-clauses we can use with the SYSTEM_TIME clause to perform different time based analysis. As follows:
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>)
This gives a lot of flexibility to how we query the data. We can query just the Orders data, just the Orders_Archive data, or both.
Some notes about table management:
- You can create indexes on either the Orders or Orders_Archive table, creating an index on one does not create it on the other. Same goes for statistics.
- Modifying a data type on the parent table Orders, will automatically modify the data type in the history table, Orders_Archive.
- Adding a new column to the Orders table will automatically add that column to the history table.
- Before you can drop a table that has versioning enabled on it, we must first disable versioning. You can drop a parent table without dropping the history table, they are independent of each other when you disable versioning.
-- Adding an index CREATE NONCLUSTERED INDEX NCIX_SalesPersonID ON dbo.Orders ( SalesPersonID ) WITH (FILLFACTOR = 90); -- Must be added to both CREATE NONCLUSTERED INDEX NCIX_SalesPersonID ON dbo.Orders_Archive ( SalesPersonID ) WITH (FILLFACTOR = 90); -- Altering the data type of an existing column or adding a new column automatically adds it to the history table ALTER TABLE dbo.Orders ALTER COLUMN Amount decimal(12,2); ALTER TABLE dbo.Orders ADD Region varchar(3); -- Disable versioning and drop the table ALTER TABLE dbo.Orders SET (SYSTEM_VERSIONING = OFF); DROP TABLE dbo.Orders; DROP TABLE dbo.Orders_Archive;
Temporal tables are a nice addition to SQL 2016, and believe or not I think they are available in standard edition. Have fun!