{"id":69895,"date":"2017-02-10T18:51:29","date_gmt":"2017-02-10T18:51:29","guid":{"rendered":"https:\/\/www.simple-talk.com\/?p=69895"},"modified":"2026-03-09T12:28:53","modified_gmt":"2026-03-09T12:28:53","slug":"sql-server-temporal-tables-recipes","status":"publish","type":"post","link":"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/database-administration-sql-server\/sql-server-temporal-tables-recipes\/","title":{"rendered":"SQL Server Temporal Tables: Practical Recipes &#038; Examples"},"content":{"rendered":"\n<p>SQL Server temporal tables (also called system-versioned tables) automatically track the full history of data changes. When you modify a row in a temporal table, SQL Server saves the previous version to a linked history table with start and end timestamps. You can then query data as it existed at any point in time using FOR SYSTEM_TIME AS OF, BETWEEN, CONTAINED IN, FROM\u2026TO, or ALL.<\/p>\n\n\n\n<p>Common use cases include audit trails (tracking who changed what and when), slowly changing dimensions (SCD Type 2), point-in-time recovery, and trend analysis. This guide provides practical recipes for creating, querying, and managing temporal tables in SQL Server 2016 and later.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-introduction\">Introduction<\/h2>\n\n\n\n<p>Temporal, or system-versioned, tables were introduced as a database feature in SQL Server 2016. This gives us a type of table that can provide information about the data that was stored at any specified time rather than just the data that is current. ANSI SQL 2011 first specified a temporal table as a database feature and this is now supported in SQL Server.<\/p>\n\n\n\n<p>The most common business uses for temporal tables are:<\/p>\n\n\n<div class=\"block-core-list\">\n<ul class=\"wp-block-list\">\n<li><strong>Slowly changing dimensions.<\/strong> The temporal tables provide a simpler way to querying data that is current for a specified period of time, such as time slicing data, that well-known problem on Data Warehousing databases.<\/li>\n\n\n\n<li><strong>Data Auditing.<\/strong> The temporal tables provide an audit trail to determine when data was modified in the \u201cparent\u201d table. This helps to meet the requirements of <strong>regulatory compliance<\/strong> and to do data forensics when needed by tracking and auditing data changes over time.<\/li>\n\n\n\n<li><strong>Repairing or recovering record level corruptions<\/strong>. Establishing a way of \u2018undoing\u2019 a data change on a table\u2019s row without downtime in case a record is accidentally deleted or updated. Therefore, the previous version of the data can be retrieved from the history table and inserted back into the \u2018parent\u2019 table. \u2013 This helps when someone (or because of some application errors) accidentally deletes data and you want to revert to it or recover it.<\/li>\n\n\n\n<li><strong>Reproducing financial reports, invoices and statements<\/strong> with the correct data for the date of issue of the document. Temporal tables allow you to query data as it was at a particular point in time to examine the state of the data as it was then.<\/li>\n\n\n\n<li><strong>Analyzing trends<\/strong> by understanding how the data changes over time with the ongoing business activity, and to calculate trends in the way that data changes over time.<\/li>\n<\/ul>\n<\/div>\n\n\n<p>In the dark days before SQL Server 2016 was introduced, the data-logging mechanism had to be established explicitly in a trigger. To give a simple example, we would need to automate the maintenance of the history for the<strong> Department<\/strong> table with following structure, starting with the Department table itself:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">CREATE TABLE dbo.Department\n    (\n    DeptID INT NOT NULL,\n    DeptName VARCHAR(50) NOT NULL,\n    ManagerID INT NULL,\n    ParentDeptID INT NULL,\n    Created DATETIME NOT NULL\n      CONSTRAINT DF_Department_Created DEFAULT GETDATE(),\n    CONSTRAINT PK_Department_DeptID PRIMARY KEY CLUSTERED(DeptID ASC) ON [PRIMARY]\n    ) ON [PRIMARY];\n  GO<\/pre>\n\n\n\n<p>The next step is to create a <strong>Department_Log<\/strong> table with two additional columns that provide the history of changes<\/p>\n\n\n<div class=\"block-core-list\">\n<ul class=\"wp-block-list\">\n<li>LogDate<\/li>\n\n\n\n<li>LogAction<\/li>\n<\/ul>\n<\/div>\n\n\n<pre class=\"wp-block-preformatted\">CREATE TABLE dbo.Department_Log\n    (\n    DeptID INT NOT NULL,\n    DeptName VARCHAR(50) NOT NULL,\n    ManagerID INT NULL,\n    ParentDeptID INT NULL,\n    Created DATETIME NOT NULL,\n    LogDate DATETIME NOT NULL,\n    LogAction VARCHAR(10) NOT NULL\n    ) ON [PRIMARY];\n  GO<\/pre>\n\n\n\n<p>When the logging \u2018history\u2019 table is ready, we can create the trigger to log the changes for the UPDATE and DELETE actions:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">CREATE TRIGGER dbo.tr_Department_Log\n  ON dbo.Department\n  FOR UPDATE, DELETE\n  AS\n    BEGIN\n    SET NOCOUNT ON;\n    IF\n      (SELECT COUNT(1)\n         FROM inserted\n           JOIN deleted\n             ON Inserted.DeptID = Deleted.DeptID\n      ) &gt; 0\n      BEGIN\n      INSERT dbo.Department_Log\n        (DeptID, DeptName, ManagerID, ParentDeptID, Created, LogDate, LogAction)\n        SELECT Deleted.DeptID, Deleted.DeptName, Deleted.ManagerID,\n          Deleted.ParentDeptID, Deleted.Created, GETDATE(), 'UPDATED'\n        FROM deleted;\n      END;\n    ELSE\n      BEGIN\n      INSERT dbo.Department_Log\n        (DeptID, DeptName, ManagerID, ParentDeptID, Created, LogDate, LogAction)\n        SELECT Deleted.DeptID, Deleted.DeptName, Deleted.ManagerID,\n          Deleted.ParentDeptID, Deleted.Created, GETDATE(), 'DELETED'\n        FROM deleted;\n      END;\n    SET NOCOUNT OFF;\n    END;\n  GO<\/pre>\n\n\n\n<p>To demonstrate how the <strong>Department_Log<\/strong> table works with the trigger, I updated three times the row where DeptID = 1, then deleted this row and finally, last update set DeptName column the to its original value.<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">update dbo.Department\n\tSET DeptName = ''\nwhere DeptID = 1 \n\nupdate dbo.Department\n\tSET DeptName = 'Engineering IT'\nwhere DeptID = 1 \n\nupdate dbo.Department\n\tSET DeptName = 'Engineering WEB'\nwhere DeptID = 1 \n\nDELETE dbo.Department where DeptID = 1\n\nINSERT dbo.Department(DeptID,  DeptName)\nSELECT  DeptID,DeptName \nFROM Department_Log WHERE DeptID = 1 and LogAction = 'DELETED'\n\nupdate dbo.Department\n\tSET DeptName = 'Engineering'\nwhere DeptID = 1 \n\nselect DeptID,  DeptName,Created,LogDate,LogAction from Department_Log\n<\/pre>\n\n\n\n<p>The result from Department_Log table shown in the next figure: <img loading=\"lazy\" decoding=\"async\" width=\"817\" height=\"167\" class=\"wp-image-69896\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2017\/02\/word-image.png\"><\/p>\n\n\n\n<p>The temporal tables feature of SQL Server 2016 can dramatically simplify the logging mechanism. This article provides step-by-step instructions on how to accomplish system-versioned tables.<\/p>\n\n\n\n<p>To migrate a table into the temporal table, a temporal table option can be set on an existing table. To create a new temporal table, you just need to set the temporal table option to ON (for example, SYSTEM_VERSIONING = ON). When the temporal table option is enabled, SQL Server 2016 generates the \u201chistorical\u201d table automatically, and internally maintains both parent and historical tables, one for storing the actual data and the other for the historical data. The temporal table\u2019s SYSTEM_TIME period columns (for example SysStartTime and SysEndTime) enables the mechanism to query data for a different time slice more efficiently. The updated or deleted data moves into the \u201chistorical\u201d table, whilst the \u201cparent\u201d table keeps the latest row version for updated records.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-what-is-the-catch\">What is the catch?<\/h2>\n\n\n\n<p>The most important considerations, restrictions and limitations of Temporal Tables are:<\/p>\n\n\n<div class=\"block-core-list\">\n<ul class=\"wp-block-list\">\n<li>In order to relate records between the Temporal Table and the history table, you must have a primary key in the Temporal Table. However, the history table cannot have a primary key.<\/li>\n\n\n\n<li>The <strong>datetime2<\/strong> datatype must be set for the SYSTEM_TIME period columns (for example <strong>SysStartTime<\/strong> and <strong>SysEndTime<\/strong>).<\/li>\n\n\n\n<li>When you create a history table, you must always specify both the schema and table name of the temporal table in the history table.<\/li>\n\n\n\n<li>The PAGE compression is the default setting for the history table.<\/li>\n\n\n\n<li>The Temporal Tables support blob data types, (nvarchar(max), varchar(max), varbinary(max), ntext, text, and image), that could affect the storage costs and have performance issues.<\/li>\n\n\n\n<li>Both temporal and history tables must be created in the same database. You cannot use a Linked Server to provide the Temporal Tables.<\/li>\n\n\n\n<li>You cannot use constraints, primary key, foreign keys or column constraints for the history tables.<\/li>\n\n\n\n<li>You cannot reference Temporal Tables in indexed views that have queries that use the FOR SYSTEM_TIME clause<\/li>\n\n\n\n<li>The SYSTEM_TIME period columns cannot be directly referenced in INSERT and UPDATE statements.<\/li>\n\n\n\n<li>You cannot use TRUNCATE TABLE while SYSTEM_VERSIONING is ON.<\/li>\n\n\n\n<li>You are not allowed to directly modify the data in a history table.<\/li>\n<\/ul>\n<\/div>\n\n\n<p>Please reference to Books Online for <a href=\"https:\/\/msdn.microsoft.com\/en-gb\/library\/mt604468.aspx?f=255&amp;MSPPError=-2147217396\">complete considerations and limitations<\/a> list:<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-creating-a-temporal-table\">Creating a Temporal Table<\/h2>\n\n\n\n<p>I have shown how to create temporal and history tables in one DDL script in Listing 1. As I mentioned earlier, the columns <strong>SysStartTime<\/strong> and <strong>SysEndTime<\/strong> with data type datetime2 for both column is required for a temporal table. The column <strong>SysStartTime<\/strong> has to be <em>GENERATED ALWAYS AS ROW START NOT NULL<\/em> specification, and <strong>SysEndTime<\/strong> has to be <em>GENERATED ALWAYS AS ROW END NOT NULL<\/em>. You\u2019re not obliged to provide a default for those columns, but I would recommend it. Both <strong>SysStartTime<\/strong> and <strong>SysEndTime<\/strong> columns have to be specified in the PERIOD FOR SYSTEM_TIME column (as MSDN defined PERIOD, in other publications PERIOD calls clause).<\/p>\n\n\n\n<p>Note: The system-versioned columns do not necessarily have to be named as <strong>SysStartTime<\/strong> and <strong>SysEndTime<\/strong>, but the column names should be selected to reflect the function of time-capturing. The options GENERATED ALWAYS AS ROW START\/END and PERIOD FOR SYSTEM_TIME (nameFrom, nameTo) enables the temporal table feature.<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">CREATE TABLE Department\n    (\n    DeptID INT NOT NULL PRIMARY KEY CLUSTERED,\n    DeptName VARCHAR(50) NOT NULL,\n    ManagerID INT NULL,\n    ParentDeptID INT NULL,\n    SysStartTime DATETIME2 GENERATED ALWAYS AS ROW START\n      CONSTRAINT DF_Department_SysStartTime DEFAULT SYSUTCDATETIME() NOT NULL,\n    SysEndTime DATETIME2 GENERATED ALWAYS AS ROW END\n      CONSTRAINT DF_Department_SysEndTime \n  \tDEFAULT CONVERT( DATETIME2, '9999-12-31 23:59:59' ) NOT NULL,\n    PERIOD FOR SYSTEM_TIME(SysStartTime, SysEndTime)\n    )\n  WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = dbo.DepartmentHistory));<\/pre>\n\n\n\n<p class=\"caption\">Listing 1: Creating the temporal and history tables<\/p>\n\n\n\n<p>After the temporal table is created, the underlined history table is created automatically, (Figure 1) as well as a CLUSTERED INDEX with both <strong>SysStartTime<\/strong> and <strong>SysEndTime<\/strong> (or the name that has been chosen to define system-versioning) column will be created for history table, Listing 2.<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">CREATE CLUSTERED INDEX ix_DepartmentHistory\n  ON dbo.DepartmentHistory\n    (SysStartTime ASC,\n    SysEndTime ASC\n    ) ON [PRIMARY];<\/pre>\n\n\n\n<p class=\"caption\">Listing 2: Creating the clustered index<\/p>\n\n\n\n<p>If a new column must be added to the temporal table, then ALTER TABLE \u2026 ADD column DDL needs to be allowed, and the new column will be mirrored automatically on the history table.<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img loading=\"lazy\" decoding=\"async\" width=\"481\" height=\"434\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2017\/02\/c-users-agrinb1-appdata-local-temp-snaghtml8c54d.png\" alt=\"C:\\Users\\AGRINB~1\\AppData\\Local\\Temp\\SNAGHTML8c54d308.PNG\" class=\"wp-image-69897\"\/><\/figure>\n\n\n\n<p><\/p>\n\n\n\n<p class=\"caption\">Figure 1: Showing newly created temporal and history tables in Object Explorer.<\/p>\n\n\n\n<p>However, it\u2019s not possible to use DROP TABLE DDL for a temporal table. First, SYSTEM_VERSIONING has to be turned OFF.<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">ALTER TABLE Department SET (SYSTEM_VERSIONING = OFF); <\/pre>\n\n\n\n<p class=\"caption\">Listing 3: Disabling SYSTEM_VERSIONING on Department table.<\/p>\n\n\n\n<p>When SYSTEM_VERSIONING is set to OFF, both temporal and history tables become regular tables. The DROP TABLE command can then be used for those tables.<\/p>\n\n\n\n<p><strong>Read also:<\/strong><br><a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/t-sql-programming-sql-server\/temporary-tables-in-sql-server\/\" target=\"_blank\" rel=\"noreferrer noopener\">Temporary tables in SQL Server<\/a><br><a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/t-sql-programming-sql-server\/beginner-guide-to-in-memory-optimized-tables-in-sql-server\/\" target=\"_blank\" rel=\"noreferrer noopener\">In-memory optimized tables in SQL Server<\/a><br><a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/t-sql-programming-sql-server\/converting-database-memory-oltp\/\" target=\"_blank\" rel=\"noreferrer noopener\">Converting a database to In-Memory OLTP<\/a><\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-setting-existing-table-to-system-versioned-table\">Setting existing table to system-versioned table<\/h2>\n\n\n\n<p>SQL Server allows you to convert an existing table into a temporal table. In order to perform this task, you need to make sure that a primary key exists on the table, and if it does not already exist, then you need to create one. Then the table has to be altered with two <strong>datetime2<\/strong> data type columns and also the option GENERATED ALWAYS AS ROW START\/END has to be applied with \u2026 <br>PERIOD FOR SYSTEM_TIME (nameFrom, nameTo). <br>Both options have to be completed with ALTER command. The second ALTER command enables the SYSTEM_VERSIONING property, and optionally (I recommend providing it explicitly), provide a name for the HISTORY_TABLE property, Listing 4.<\/p>\n\n\n\n<p>For example, let\u2019s set the existing table <strong>Department_Exist<\/strong> to a temporal table. Run Listing 4, then run Listing 5. Refresh the Table icon to see the result, as in Figure 2.<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">CREATE TABLE Department_Exist (    \n       DeptID int NOT NULL PRIMARY KEY CLUSTERED  \n     , DeptName varchar(50) NOT NULL  \n     , ManagerID INT  NULL  \n     , ParentDeptID int NULL )  <\/pre>\n\n\n\n<p class=\"caption\">Listing 4: Creating table Department_Exist.<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">ALTER TABLE dbo.Department_Exist\n  ADD SysStartTime datetime2 GENERATED ALWAYS AS ROW START  \n  CONSTRAINT DF_Department_Exist_SysStartTime DEFAULT SYSUTCDATETIME() NOT NULL,\n  SysEndTime datetime2 GENERATED ALWAYS AS ROW END \n  CONSTRAINT DF_Department_Exist_SysEndTime DEFAULT CONVERT (DATETIME2, '9999-12-31 23:59:59') NOT NULL,\n         PERIOD FOR SYSTEM_TIME (SysStartTime, SysEndTime)\n  GO\n   \n  ALTER TABLE dbo.Department_Exist\n      SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = dbo.Department_ExistHistory))\n  GO<\/pre>\n\n\n\n<p class=\"caption\">Listing 5: Adding the system-versioned columns, and enable the system-versioned on the <strong>Department_Exist<\/strong> table.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td>\n<figure><img loading=\"lazy\" decoding=\"async\" width=\"243\" height=\"93\" class=\"wp-image-69898\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2017\/02\/word-image-1.png\"><\/figure><p><\/p>\n<p>Existing table<\/p>\n<\/td><td>\n<figure><img loading=\"lazy\" decoding=\"async\" width=\"287\" height=\"114\" class=\"wp-image-69899\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2017\/02\/word-image-2.png\"><\/figure><p><\/p>\n<p>Converted to temporal table<\/p>\n<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"caption\">Figure 2: Comparing side-by-side <strong>Department_Exist<\/strong> after converted to a Temporal table.<\/p>\n\n\n\n<p>Check the metadata of the temporal tables:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">-- List temporal tables, temporal_type = 2  \n  SELECT tables.object_id, temporal_type, temporal_type_desc, history_table_id,\n    tables.name\n    FROM sys.tables\n    WHERE temporal_type = 2 -- SYSTEM_VERSIONED_TEMPORAL_TABLE\n  -- List temporal tables and history tables\n  SELECT h.name temporal_name, h.temporal_type_desc, h.temporal_type,\n    t.name AS history_table_name, t.temporal_type, t.temporal_type_desc\n    FROM sys.tables t\n      JOIN sys.tables h\n        ON t.object_id = h.history_table_id<\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-converting-an-in-memory-optimized-oltp-table-to-a-system-versioned-table\">Converting an In-Memory Optimized OLTP Table to a System-Versioned Table<\/h2>\n\n\n\n<p>Although the process of converting an <a href=\"https:\/\/www.simple-talk.com\/sql\/database-administration\/migrating-disk-based-table-memory-optimized-table-sql-server\/\"> In-Memory Optimized OLTP<\/a> table to a system-versioned table is similar, there are some differences that we need to cover and demonstrate in this section.<\/p>\n\n\n\n<p>You need to be aware of some specific details when converting the in-memory optimized table to the system-versioned table:<\/p>\n\n\n<div class=\"block-core-list\">\n<ul class=\"wp-block-list\">\n<li>The In-memory optimized tables must be durable (DURABILITY = SCHEMA_AND_DATA).<\/li>\n\n\n\n<li>In-Memory optimized history table is created as disk-based.<\/li>\n\n\n\n<li>Queries that affect only the parent table can be used in natively-compiled T-SQL modules. You cannot use Temporal queries using the FOR SYSTEM TIME clause in natively compiled modules, but it is possible to use the FOR SYSTEM TIME clause with in-memory optimized tables in ad hoc queries and non-native modules.<\/li>\n\n\n\n<li>An internal In-memory optimized <strong>staging table<\/strong> is automatically created to accept the most recent changes (INSERT, DELETE) on changes to the in-memory optimized parent table when <strong>SYSTEM_VERSIONING = ON<\/strong>.<\/li>\n\n\n\n<li>Data from the internal In-memory optimized staging table is regularly moved to the disk-based history table by the asynchronous data flush task. This data flush mechanism has a goal of keeping the internal memory buffers at less than 10% of the memory consumption of their parent objects. DMV <strong>sys.dm_db_xtp_memory_consumers<\/strong> will help to track the total memory consumption.<\/li>\n\n\n\n<li>A data flush can enforce a by calling sys.sp_xtp_flush_temporal_history @schema_name, @object_name stored procedure.<\/li>\n\n\n\n<li>When SYSTEM_VERSIONING = OFF, or when the schema of the system-versioned table is modified by adding, dropping or altering columns, the entire contents of the internal staging buffer is moved into the disk-based history table.<\/li>\n\n\n\n<li>Querying of historical data is, effectively, done under SNAPSHOT isolation level and always returns a union between in-memory staging buffer and the disk-based table without duplicates.<\/li>\n\n\n\n<li>ALTER TABLE operations that change the table schema internally must perform a data flush, which may slow down the operation.<\/li>\n<\/ul>\n<\/div>\n\n\n<h2 class=\"wp-block-heading\" id=\"h-creating-new-in-memory-optimized-oltp-with-system-versioned-table-option-enabled\">Creating new In-Memory Optimized OLTP with System-Versioned Table option enabled<\/h2>\n\n\n\n<p>The DDL for creating a new in-memory optimized table with the temporal table options is very close in its syntax to a traditional disk-based table. The in-memory optimized table syntax has the WITH block to set MEMORY_OPTIMIZED and DURABILITY properties initially. Therefore, the SYSTEM_VERSIONING property needs to be added separated by a comma, as in Listing 7.<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">CREATE TABLE dbo.InMemory\n  (\n  \tUniqueName varchar(50) NOT NULL \n  \t\tPRIMARY KEY NONCLUSTERED HASH WITH (BUCKET_COUNT = 131072),\n  \tCity varchar(32) NULL,\n  \tState_Province varchar(32) NULL,\n  \tLastModified datetime NOT NULL\n  \t   , SysStartTime datetime2 GENERATED ALWAYS AS ROW START\n   CONSTRAINT DF_InMemory_SysStartTime DEFAULT GETDATE() NOT NULL\n     , SysEndTime datetime2 GENERATED ALWAYS AS ROW END\n   CONSTRAINT DF_InMemory_SysEndTime\n  \tDEFAULT CONVERT (DATETIME2, '9999-12-31 23:59:59') NOT NULL\n     , PERIOD FOR SYSTEM_TIME (SysStartTime,SysEndTime)\n  )\n  WITH \n  ( \n      MEMORY_OPTIMIZED = ON , DURABILITY = SCHEMA_AND_DATA,\n      SYSTEM_VERSIONING = ON (HISTORY_TABLE = dbo.InMemory_History) \n  )<\/pre>\n\n\n\n<p class=\"caption\">Listing 7: Creating new In-Memory Optimized OLTP with System-Versioned Table option enabled<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-adding-the-system-versioned-table-option-to-an-existing-in-memory-optimized-oltp-table\">Adding the System-Versioned Table option to an existing In-Memory Optimized OLTP Table.<\/h2>\n\n\n\n<p>It is more difficult to convert an existing in-memory optimized OLTP table to the system-versioned table, as we show in Listing 8.<\/p>\n\n\n\n<p>To demonstrate this mechanism let\u2019s create the table:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">CREATE TABLE dbo.InMemoryExist\n  (\n  \tUniqueName varchar(50) NOT NULL \n  \t\tPRIMARY KEY NONCLUSTERED HASH WITH (BUCKET_COUNT = 131072),\n  \tCity varchar(32) NULL,\n  \tState_Province varchar(32) NULL\n  )\n  WITH \n  ( \n      MEMORY_OPTIMIZED = ON , DURABILITY = SCHEMA_AND_DATA\n  )<\/pre>\n\n\n\n<p class=\"caption\">Listing 8: Creating new In-Memory Optimized OLTP table<\/p>\n\n\n\n<p>When the table has been created, we need to add the temporal table options before any data is added to the table, as in Listing 9:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">ALTER TABLE dbo.InMemoryExist\n  ADD SysStartTime datetime2 GENERATED ALWAYS AS ROW START  \n  CONSTRAINT DF_InMemoryExist_SysStartTime DEFAULT SYSUTCDATETIME() NOT NULL,\n  SysEndTime datetime2 GENERATED ALWAYS AS ROW END \n  CONSTRAINT DF_InMemoryExist_SysEndTime DEFAULT CONVERT (DATETIME2, '9999-12-31 23:59:59') NOT NULL,\n         PERIOD FOR SYSTEM_TIME (SysStartTime, SysEndTime)\n  GO\n   \n  ALTER TABLE dbo.InMemoryExist\n      SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = dbo.InMemoryExist_History))\n  GO<\/pre>\n\n\n\n<p class=\"caption\">Listing 9: Adding temporal table options<\/p>\n\n\n\n<p>If the table already contains data, then the process of converting the table to the system-versioned table is more complicated.<\/p>\n\n\n\n<p>If <strong>InMemoryExist<\/strong> was created with the system-versioned option, then we need to drop the <strong>InMemoryExist<\/strong> and <strong>InMemoryExist_History<\/strong> tables:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">ALTER TABLE InMemoryExist set (SYSTEM_VERSIONING = OFF)\n  GO\n  DROP TABLE InMemoryExist\n  GO\n  DROP TABLE InMemoryExist_History\n  GO<\/pre>\n\n\n\n<p>We recreate the table (use the code sample, Listing 8, provided above in this section). Then we insert data into the <strong>InMemoryExist<\/strong> table:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">INSERT InMemoryExist\n  select NEWID(),name, type_desc from sys.objects where is_ms_shipped = 0<\/pre>\n\n\n\n<p>When you run the code to add temporal table options, listing 9, then the following errors will be thrown:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">Msg 13575, Level 16, State 0, Line 21\n  ADD PERIOD FOR SYSTEM_TIME failed because table 'database.dbo.InMemoryExist' contains records where end of period is not equal to MAX datetime.\n  Msg 13510, Level 16, State 1, Line 29<\/pre>\n\n\n\n<p>Cannot set SYSTEM_VERSIONING to ON when SYSTEM_TIME period is not defined.<\/p>\n\n\n\n<p>To avoid these errors, we need to add system-versioning options in more detailed steps:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">--Step 1. Adding nullable the columns\n  ALTER TABLE dbo.InMemoryExist ADD SysStartTime datetime2 NULL  \n  GO\n  ALTER TABLE dbo.InMemoryExist ADD SysEndTime datetime2 NULL  \n  GO\n  --Step 2 Adding the default constraints\n  ALTER TABLE InMemoryExist ADD CONSTRAINT DF_InMemoryExist_SysStartTime DEFAULT GETDATE() FOR SysStartTime;\n  GO\n  ALTER TABLE InMemoryExist ADD CONSTRAINT DF_InMemoryExist_SysEndTime DEFAULT CAST('9999-12-31 23:59:59.9999999' AS DATETIME2) FOR SysEndTime;\n  --Step 3 Updating the column\n  UPDATE dbo.InMemoryExist \n  \tSET SysStartTime = '19000101 00:00:00.0000000'\n  \t\t,SysEndTime = '99991231 23:59:59.9999999'\n  GO\n  --Step 4 Setting NOT NULL to the columns\n  ALTER TABLE dbo.InMemoryExist ALTER COLUMN SysStartTime datetime2 NOT NULL  \n  GO\n  ALTER TABLE dbo.InMemoryExist ALTER COLUMN SysEndTime datetime2 NOT NULL  \n  GO\n  --Step 5 Adding PERIOD FOR SYSTEM_TIME option\n  ALTER TABLE InMemoryExist ADD PERIOD FOR SYSTEM_TIME (SysStartTime, SysEndTime)\n  GO\n  --Step 6 Setting SYSTEM_VERSIONING property\n  ALTER TABLE InMemoryExist\n      SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = dbo.InMemoryExist_History))\n  GO <\/pre>\n\n\n\n<p>Now the <strong>InMemoryExist<\/strong> table is enabled for system-versioning processes.<\/p>\n\n\n\n<p><strong>Read also:<\/strong> <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/theory-and-design\/state-transition-constraints-2\/\" target=\"_blank\" rel=\"noreferrer noopener\">State transition constraints<\/a><\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-specifying-data-consistency-check-property\">Specifying DATA_CONSISTENCY_CHECK property<\/h2>\n\n\n\n<p>You ought to set SYSTEM_VERSIONING with DATA_CONSISTENCY_CHECK = ON to enforce data consistency checks on existing data. However, the DATA_CONSISTENCY_CHECK property <a href=\"https:\/\/support.microsoft.com\/en-us\/help\/3174708\/a-memory-leak-occurs-when-data-consistency-check-is-being-executed-for-a-system-versioned-temporal-table-in-sql-server-2016\">currently has a memory leak profile when it used<\/a> . If you decided to enable DATA_CONSISTENCY_CHECK for the temporal tables, then make sure that your instance has Cumulative Update 1 for SQL Server 2016.<\/p>\n\n\n\n<p>Here is an example of enabling the DATA_CONSISTENCY_CHECK property on existing tables:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">ALTER TABLE InMemoryExist\n      SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = dbo.TableName_History, DATA_CONSISTENCY_CHECK = ON))\n  For a new table, DATA_CONSISTENCY_CHECK property enables after HISTORY_TABLE property separated by a comma.\n  WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = dbo.DepartmentHistory, DATA_CONSISTENCY_CHECK = ON));<\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-conclusion\">Conclusion<\/h2>\n\n\n\n<p>The Temporal Table is a very useful SQL Server 2016 feature with which to automate row-versioned processes. It simplifies the task of archiving data and can also be a real fix to utilize a slowly-changing dimension for the data warehouse databases. Because it is so easy to set up new as well as existing tables, the Temporal Table feature is a good choice to implement with SQL Server databases.<\/p>\n\n\n\n<section id=\"my-first-block-block_3edacaf3f2cd668eed096943108238a0\" class=\"my-first-block alignwide\">\n    <div class=\"bg-brand-600 text-base-white py-5xl px-4xl rounded-sm bg-gradient-to-r from-brand-600 to-brand-500 red\">\n        <div class=\"gap-4xl items-start md:items-center flex flex-col md:flex-row justify-between\">\n            <div class=\"flex-1 col-span-10 lg:col-span-7\">\n                <h3 class=\"mt-0 font-display mb-2 text-display-sm\">Fast, reliable and consistent SQL Server development&#8230;<\/h3>\n                <div class=\"child:last-of-type:mb-0\">\n                                            &#8230;with SQL Toolbelt Essentials. 10 ingeniously simple tools for accelerating development, reducing risk, and standardizing workflows.                                    <\/div>\n            <\/div>\n                            <a href=\"https:\/\/www.red-gate.com\/products\/sql-toolbelt-essentials\/\" class=\"btn btn--secondary btn--lg\">Learn more &amp; try for free<\/a>\n                    <\/div>\n    <\/div>\n<\/section>\n\n\n<section id=\"faq\" class=\"faq-block my-5xl\">\n    <h2>FAQs: Temporal Tables in SQL Server<\/h2>\n\n                        <h3 class=\"mt-4xl\">1. How do you create a temporal table in SQL Server?<\/h3>\n            <div class=\"faq-answer\">\n                <p>Add two DATETIME2 columns for the period start and end, define a PERIOD FOR SYSTEM_TIME clause, and enable SYSTEM_VERSIONING with a history table: CREATE TABLE dbo.Employee (\u2026 SysStart datetime2 GENERATED ALWAYS AS ROW START, SysEnd datetime2 GENERATED ALWAYS AS ROW END, PERIOD FOR SYSTEM_TIME (SysStart, SysEnd)) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = dbo.EmployeeHistory)). SQL Server automatically manages the history table.<\/p>\n            <\/div>\n                    <h3 class=\"mt-4xl\">2. How do you query temporal table history in SQL Server?<\/h3>\n            <div class=\"faq-answer\">\n                <p>Use the FOR SYSTEM_TIME clause: AS OF (point-in-time snapshot), FROM\u2026TO (open interval), BETWEEN (inclusive), CONTAINED IN (fully within range), or ALL (entire history). Example: SELECT * FROM dbo.Employee FOR SYSTEM_TIME AS OF &#8216;2024-01-15&#8217; returns data as it existed on that date.<\/p>\n            <\/div>\n                    <h3 class=\"mt-4xl\">3. What is the difference between temporal tables in SQL Server and PostgreSQL?<\/h3>\n            <div class=\"faq-answer\">\n                <p>SQL Server temporal tables are built into the engine with automatic history management via SYSTEM_VERSIONING. PostgreSQL does not have native temporal tables in the SQL Server sense; instead, you implement temporal functionality using triggers, range types (tstzrange), and extensions like temporal_tables or pg_temporal. The concept is the same but the implementation differs significantly.<\/p>\n            <\/div>\n            <\/section>\n","protected":false},"excerpt":{"rendered":"<p>Practical recipes for SQL Server temporal tables: create system-versioned tables, query historical data with FOR SYSTEM_TIME, manage retention, and implement slowly changing dimensions and audit trails.&hellip;<\/p>\n","protected":false},"author":272577,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[143527,143525],"tags":[],"coauthors":[41240],"class_list":["post-69895","post","type-post","status-publish","format-standard","hentry","category-database-administration-sql-server","category-learn"],"acf":[],"_links":{"self":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/69895","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/users\/272577"}],"replies":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/comments?post=69895"}],"version-history":[{"count":11,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/69895\/revisions"}],"predecessor-version":[{"id":109045,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/69895\/revisions\/109045"}],"wp:attachment":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/media?parent=69895"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/categories?post=69895"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/tags?post=69895"},{"taxonomy":"author","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/coauthors?post=69895"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}