Window functions and date calculations make a powerful combination when you need to analyze patterns over time. One interesting pairing is DATEDIFF() with the LAG() function, which lets you compare each row’s date against the previous row’s date within an ordered dataset. This can be handy for calculating time gaps between sequential events like maintenance intervals, customer order frequency, or processing delays.
The LAG() function retrieves a value from a previous row in the result set without requiring a self-join. When you combine it with DATEDIFF(), you can measure the time elapsed between consecutive events in a single pass through your data. This approach is both more readable and more performant than traditional self-join methods.
Example
Let’s say you’re monitoring database backup operations and want to analyze the time gaps between consecutive backups. Understanding these intervals helps you identify irregular backup schedules, potential system issues, or compliance with your backup policies.
Sample Data
Here’s some sample data we can use for our example:
-- Create the backup log table
CREATE TABLE BackupLog (
BackupID INT PRIMARY KEY IDENTITY(1,1),
DatabaseName VARCHAR(100),
BackupType VARCHAR(20),
BackupStartTime DATETIME,
BackupSizeGB DECIMAL(10,2)
);
-- Insert sample data with varying backup intervals
INSERT INTO BackupLog (DatabaseName, BackupType, BackupStartTime, BackupSizeGB)
VALUES
('ProductionDB', 'Full', '2025-11-01 02:00:00', 145.8),
('ProductionDB', 'Differential', '2025-11-02 02:00:00', 23.4),
('ProductionDB', 'Differential', '2025-11-03 02:00:00', 28.1),
('ProductionDB', 'Differential', '2025-11-04 02:00:00', 31.7),
('ProductionDB', 'Full', '2025-11-08 02:00:00', 152.3),
('ProductionDB', 'Differential', '2025-11-09 02:00:00', 19.6),
('ProductionDB', 'Differential', '2025-11-10 02:00:00', 25.9),
('ProductionDB', 'Full', '2025-11-15 02:00:00', 158.7);
The Query
Now here’s where we get LAG() and DATEDIFF() to work together:
SELECT DatabaseName,
BackupType,
BackupStartTime,
LAG(BackupStartTime) OVER (ORDER BY BackupStartTime) AS PreviousBackupTime,
DATEDIFF(hour,
LAG(BackupStartTime) OVER (ORDER BY BackupStartTime),
BackupStartTime) AS HoursSincePreviousBackup
FROM BackupLog
WHERE DatabaseName = 'ProductionDB';
Result:
DatabaseName BackupType BackupStartTime PreviousBackupTime HoursSincePreviousBackup
------------ ------------ --------------- ------------------ ------------------------
ProductionDB Full 2025-11-01 null null
ProductionDB Differential 2025-11-02 2025-11-01 24
ProductionDB Differential 2025-11-03 2025-11-02 24
ProductionDB Differential 2025-11-04 2025-11-03 24
ProductionDB Full 2025-11-08 2025-11-04 96
ProductionDB Differential 2025-11-09 2025-11-08 24
ProductionDB Differential 2025-11-10 2025-11-09 24
ProductionDB Full 2025-11-15 2025-11-10 120
This query shows each backup alongside the timestamp of the previous backup and calculates how many hours elapsed between them. The first row shows NULL for both the previous backup time and hours since previous backup because there’s no prior backup to compare against.
Understanding the Window Function
The LAG(BackupStartTime) OVER (ORDER BY BackupStartTime) expression looks at each row and retrieves the BackupStartTime value from the row immediately before it when sorted by BackupStartTime. The OVER clause with ORDER BY is required, as it defines how the rows are sequenced for the LAG() function to work properly.
When DATEDIFF() wraps around this LAG() expression, it’s comparing the current row’s BackupStartTime against the previous row’s BackupStartTime. Notice we use the same LAG() expression twice in the query – once to display the previous timestamp and once inside DATEDIFF() to calculate the difference. SQL Server is smart enough to evaluate this efficiently.
I should mention that our example uses the default LAG() offset of 1. It’s also possible to specify the offset by providing a second argument. This enables you to go back multiple rows. In other words, you aren’t limited to just the previous row – you can go back any number of rows. For example, you could do the following to go back two rows:
LAG(BackupStartTime, 2)
Handling Multiple Databases
If you’re tracking backups for multiple databases, you’ll want to partition your window function so each database’s backups are analyzed separately.
Let’s add some more rows, for another database:
-- Add data for another database
INSERT INTO BackupLog (DatabaseName, BackupType, BackupStartTime, BackupSizeGB)
VALUES
('AnalyticsDB', 'Full', '2025-11-02 03:00:00', 89.2),
('AnalyticsDB', 'Differential', '2025-11-03 03:00:00', 12.5),
('AnalyticsDB', 'Full', '2025-11-09 03:00:00', 94.8);
Now for the query:
SELECT DatabaseName,
BackupType,
BackupStartTime,
DATEDIFF(hour,
LAG(BackupStartTime) OVER (PARTITION BY DatabaseName ORDER BY BackupStartTime),
BackupStartTime) AS HoursSincePreviousBackup
FROM BackupLog
ORDER BY DatabaseName, BackupStartTime;
Result:
DatabaseName BackupType BackupStartTime HoursSincePreviousBackup
------------ ------------ --------------- ------------------------
AnalyticsDB Full 2025-11-02 null
AnalyticsDB Differential 2025-11-03 24
AnalyticsDB Full 2025-11-09 144
ProductionDB Full 2025-11-01 null
ProductionDB Differential 2025-11-02 24
ProductionDB Differential 2025-11-03 24
ProductionDB Differential 2025-11-04 24
ProductionDB Full 2025-11-08 96
ProductionDB Differential 2025-11-09 24
ProductionDB Differential 2025-11-10 24
ProductionDB Full 2025-11-15 120
The PARTITION BY DatabaseName clause resets the LAG() function for each database, ensuring that the first backup of ProductionDB isn’t compared against the last backup of AnalyticsDB. Each database gets its own independent sequence.
Finding Anomalies
This pattern can be especially useful when you’re looking for unusual gaps. You can wrap the query in a CTE() or subquery and filter for intervals that exceed your expected threshold:
WITH BackupIntervals AS (
SELECT DatabaseName,
BackupType,
BackupStartTime,
DATEDIFF(hour,
LAG(BackupStartTime) OVER (PARTITION BY DatabaseName ORDER BY BackupStartTime),
BackupStartTime) AS HoursSincePreviousBackup
FROM BackupLog
)
SELECT *
FROM BackupIntervals
WHERE HoursSincePreviousBackup > 48
OR HoursSincePreviousBackup IS NULL;
Result:
DatabaseName BackupType BackupStartTime HoursSincePreviousBackup
------------ ---------- --------------- ------------------------
AnalyticsDB Full 2025-11-02 null
AnalyticsDB Full 2025-11-09 144
ProductionDB Full 2025-11-01 null
ProductionDB Full 2025-11-08 96
ProductionDB Full 2025-11-15 120
This quickly identifies any backups that occurred more than 48 hours after the previous one (or where it’s NULL), which might indicate a missed backup window or system downtime.
Other Window Functions with DATEDIFF()
While LAG() is the most common pairing, you can also use LEAD() to calculate how long until the next event, or FIRST_VALUE() to compare every row against the first occurrence in a partition. The pattern is the same – the window function retrieves a date value, and DATEDIFF() calculates the difference. This approach eliminates the need for complex self-joins and keeps your queries clean and maintainable.