SQL Server Temporal Tables Advanced Query Strategies
In parts one and two of this series we learned how to create or convert a temporal table and basics on how to query history. Once you start using temporal tables it doesn’t take long before you are pushing the basics, this is some of the tricks that I’ve used to get the most out of temporal tables.
Part 1 – Introduction to SQL Server Temporal Tables
Part 2 – Converting an Existing SQL Server Temporal Table
Part 3 – SQL Server Temporal Tables Advanced Query Strategies
Saving Data
Saving data in temporal tables is the same as normal tables but it is useful to control when a history record is written. Let’s say your application is using a standard UPDATE SQL statement and a user double clicks the save button by accident or some other scenario where the update is executed twice. What does the history table look like then? The answer is you will get two history records for one change.
MERGE [ApplicationUser] AS target
USING (SELECT @userId, @firstName, @lastName, @jobTitle, @lastUpdatedBy)
AS source (UserId, FirstName, LastName, JobTitle, LastUpdatedBy)
ON target.UserId = source.UserId
WHEN MATCHED AND target.FirstName <> source.FirstName OR
target.LastName <> source.LastName OR target.JobTitle <> source.JobTitle
THEN UPDATE SET FirstName = source.FirstName, LastName = source.LastName, JobTitle = source.JobTitle, LastUpdatedBy = source.LastUpdatedBy;
One last note on saving data is that the ValidFrom is timestamp is tied to the transaction scope so if you are updating multiple rows or multiple tables in one transaction, they will have the same ValidFrom which can be very useful.
Querying Across Joined Tables
Let’s add another table to our example to demonstrate some more advanced querying and add a table for user addresses.
CREATE TABLE [dbo].[ApplicationUserAddress] (
[UserId] NVARCHAR (20) NOT NULL,
[StreetAddress] NVARCHAR (50) NULL,
[City] NVARCHAR (50) NULL,
[State] NVARCHAR (50) NULL,
[PostalCode] NVARCHAR (10) NULL,
[Country] NVARCHAR (50) NULL,
[ValidFrom] datetime2(7) GENERATED ALWAYS AS ROW START NOT NULL,
[ValidTo] datetime2(7) GENERATED ALWAYS AS ROW END NOT NULL,
PERIOD FOR SYSTEM_TIME ([ValidFrom], [ValidTo]),
CONSTRAINT [PK_ApplicationUserAddress] PRIMARY KEY CLUSTERED ([UserId] ASC),
CONSTRAINT [FK_ApplicationUserAddress_ApplicationUser] FOREIGN KEY ([UserId]) REFERENCES [dbo].[ApplicationUser] ([UserId])
)
WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE=[dbo].[ApplicationUserAddressHistory], DATA_CONSISTENCY_CHECK=ON));
SELECT au.FirstName,
aua.StreetAddress,
au.ValidFrom as UserValidFrom,
aua.ValidFrom as AddressValidFrom
FROM [dbo].[ApplicationUser] au
LEFT JOIN [dbo].[ApplicationUserAddress] aua on aua.UserId = au.UserId
WHERE au.UserId = '1234'
SELECT au.FirstName,
aua.StreetAddress,
au.ValidFrom as UserValidFrom,
aua.ValidFrom as AddressValidFrom
FROM [dbo].[ApplicationUser] FOR SYSTEM_TIME ALL au
LEFT JOIN [dbo].[ApplicationUserAddress] aua on aua.UserId = au.UserId
WHERE au.UserId = '1234'
ORDER BY au.ValidFrom DESC
DECLARE @asOf DATETIME2 = '2022-11-21 23:41:26.5846378';
SELECT au.FirstName,
aua.StreetAddress,
au.ValidFrom as UserValidFrom,
aua.ValidFrom as AddressValidFrom
FROM [dbo].[ApplicationUser] FOR SYSTEM_TIME AS OF @asOf au
LEFT JOIN [dbo].[ApplicationUserAddress] FOR SYSTEM_TIME AS OF @asOf aua on aua.UserId = au.UserId
WHERE au.UserId = '1234'
ORDER BY au.ValidFrom DESC
SELECT au.FirstName,
aua.StreetAddress,
au.ValidFrom as UserValidFrom,
aua.ValidFrom as AddressValidFrom
FROM [dbo].[ApplicationUser] FOR SYSTEM_TIME ALL au
LEFT JOIN [dbo].[ApplicationUserAddress] FOR SYSTEM_TIME AS OF vs.ValidFrom aua on aua.UserId = au.UserId
WHERE au.UserId = '1234'
ORDER BY au.ValidFrom DESC
CREATE OR ALTER FUNCTION [dbo].[AddressAsOfDate] (@userId nvarchar(20), @asOfDate datetime2)
RETURNS TABLE
AS RETURN
SELECT *
FROM [dbo].[ApplicationUserAddress] FOR SYSTEM_TIME AS OF @asOfDate
WHERE UserId = @userId
SELECT au.FirstName,
aua.StreetAddress,
au.ValidFrom as UserValidFrom,
aua.ValidFrom as AddressValidFrom
FROM [dbo].[ApplicationUser] FOR SYSTEM_TIME ALL au
CROSS APPLY [dbo].[AddressAsOfDate] (au.UserId, au.ValidFrom) aua
WHERE au.UserId = '1234'
ORDER BY au.ValidFrom DESC
CREATE OR ALTER FUNCTION [dbo].[AddressAsOfDate] (@userId nvarchar(20), @asOfDate datetime2)
RETURNS TABLE
AS RETURN
SELECT aua.*
FROM [dbo].[ApplicationUser] au
LEFT JOIN [dbo].[ApplicationUserAddress] FOR SYSTEM_TIME AS OF @asOfDate aua ON aua.UserId = au.UserId
WHERE au.UserId = @userId
Conclusion
With these saving tricks you should be able to minimize unnecessary records in the history table and should be able to join temporal tables to maximum effect.
Part 1 – Introduction to SQL Server Temporal Tables
Part 2 – Converting an Existing SQL Server Temporal Table
Part 3 – SQL Server Temporal Tables Advanced Query Strategies
About Intertech
Intertech is a Software Development Consulting Firm that provides single and multiple turnkey software development teams, available on your schedule and configured to achieve success as defined by your requirements independently or in co-development with your team. Intertech teams combine proven full-stack, DevOps, Agile-experienced lead consultants with Delivery Management, User Experience, Software Development, and QA experts in Business Process Automation (BPA), Microservices, Client- and Server-Side Web Frameworks of multiple technologies, Custom Portal and Dashboard development, Cloud Integration and Migration (Azure and AWS), and so much more. Each Intertech employee leads with the soft skills necessary to explain complex concepts to stakeholders and team members alike and makes your business more efficient, your data more valuable, and your team better. In addition, Intertech is a trusted partner of more than 4000 satisfied customers and has a 99.70% “would recommend” rating.