If you need a scheme to find rows for a given time range ordered by ID
, but don't want to create a computed column or an index that might be bigger than the original table, there is a way using an indexed view, assuming some correlation between ID
values and the date.
The general idea of the solution below is to maintain a distinct list of dates and associated ID ranges for each TypeID
using an indexed view (which is always synchronized with the base table in SQL Server). Aggregating IDs into ranges means the indexed view can be very much smaller than the base table.
Given a range of dates to query, we can find the ID ranges we need to inspect from the indexed view, then check for exact matches in the base table using a seek on the existing clustered index. Sorting the small view list by ID range allows us to find ordered matching rows efficiently.
This scheme works best when many IDs in a range occur for each day and TypeID
. The worst case is when all IDs are separated by more than the range size from every other value per day and TypeID
. The range size used in this example is 1000.
Aside from the required unique clustered index on the small indexed view, no new columns or indexes are needed for this solution.
Test table and data
This script creates 84,067,200 rows of data, requiring 1GB of storage with page compression. There's a row for each second between 1 January 2020 and 31 August 2022.
CREATE TABLE dbo.TableName
(
ID bigint IDENTITY NOT NULL,
TypeID integer NOT NULL,
DateTimeUTC datetime NOT NULL,
CONSTRAINT [PK dbo.TableName ID]
PRIMARY KEY CLUSTERED (ID)
WITH (DATA_COMPRESSION = PAGE)
);
-- Add 84,067,200 rows
WITH N AS
(
SELECT TOP
(
DATEDIFF(SECOND,
CONVERT(datetime, '2020-01-01', 120),
CONVERT(datetime, '2022-08-31', 120))
)
n = ROW_NUMBER() OVER (ORDER BY @@SPID)
FROM sys.all_columns AS AC1
CROSS JOIN sys.all_columns AS AC2
ORDER BY n
)
INSERT dbo.TableName WITH (TABLOCK)
(TypeID, DateTimeUTC)
SELECT
TypeID = 2,
DateTimeUTC =
DATEADD(SECOND, N.n,
CONVERT(datetime, '2020-01-01', 120))
FROM N;
Indexed view
The view contains one row for each TypeID
, date, and range of up to 1000 IDs. There's nothing special about the 1000 number—you can experiment with different range sizes by changing the constants and rebuilding the view.
CREATE OR ALTER VIEW dbo.TableNameSummary
WITH SCHEMABINDING AS
SELECT
TN.TypeID,
DateOnly = CONVERT(date, TN.DateTimeUTC),
IDrange1K = FLOOR(TN.ID / 1000) * 1000,
NumRows = COUNT_BIG(*)
FROM dbo.TableName AS TN
GROUP BY
TN.TypeID,
CONVERT(date, TN.DateTimeUTC),
FLOOR(TN.ID / 1000) * 1000;
GO
CREATE UNIQUE CLUSTERED INDEX
[CUQ dbo.TableNameSummary TypeID, DateOnly, IDrange1K]
ON dbo.TableNameSummary
(TypeID, DateOnly, IDrange1K)
WITH (DATA_COMPRESSION = PAGE);
The view contains 84,847 rows in 952KB for this example.
Query
DECLARE
@Rows bigint = 100,
@StartDate datetime = CONVERT(datetime, '2022-04-26 07:36:36', 120),
@EndDate datetime = CONVERT(datetime, '2022-08-04 07:02:40', 120);
WITH Ranges AS
(
-- Low end of ID ranges from the indexed view.
-- DISTINCT because a range may have data for several days.
SELECT DISTINCT TNS.IDrange1K
FROM dbo.TableNameSummary AS TNS WITH (NOEXPAND)
WHERE
TNS.TypeID = 2
AND TNS.DateOnly >= CONVERT(date, @StartDate)
AND TNS.DateOnly <= CONVERT(date, @EndDate)
)
SELECT TOP (@Rows)
MR.ID
FROM Ranges AS R
CROSS APPLY
(
-- Find matching rows in each ID range
SELECT TOP (@Rows) TN.ID
FROM dbo.TableName AS TN
WHERE
-- Match ID group from indexed view
TN.ID >= R.IDrange1K
AND TN.ID < (R.IDrange1K + 1000)
-- Must check individual rows match
-- the exact date & time range
AND TN.DateTimeUTC > @StartDate
AND TN.DateTimeUTC < @EndDate
ORDER BY
TN.ID ASC
) AS MR
ORDER BY
R.IDrange1K ASC,
MR.ID ASC;
Results are returned in 11ms with a serial row-mode execution plan:

The distinct sort is applied to 8,807 rows from the indexed view in this example. The base table seek finds 27,897 rows before applying a residual predicate for exact date & time range matches. This is due to many rows on the first date falling before the start time at the low end of the desired range.
Example execution plans
Original query:

With Erik's index:

With Dan's index:

With the indexed view (as above):

Using Joe's scheme:

Performance summary
Tested on SQL Server 2019 CU16-GDR. Parallelism enabled where it's beneficial, MAXDOP 12.
Table size (with page compression) 1,026,312KB.
Best result in each group in bold.
Author |
Time (ms) |
Extra Index Size (KB) |
Compression |
Parallel |
Original |
1364 |
|
|
Yes |
|
|
|
|
|
Erik |
359 |
2,170,296 |
None |
Yes |
Erik |
590 |
1,493,784 |
Row |
Yes |
Erik |
974 |
1,026,000 |
Page |
Yes |
|
|
|
|
|
Dan |
275 |
2,170,296 |
None |
Yes |
Dan |
311 |
1,493,784 |
Row |
Yes |
Dan |
387 |
1,026,000 |
Page |
Yes |
|
|
|
|
|
Paul |
10 |
2,712 |
None |
No |
Paul |
11 |
1,344 |
Row |
No |
Paul |
11 |
952 |
Page |
No |
|
|
|
|
|
Joe |
4 |
2,418,640 |
None |
No |
Joe |
5 |
1,742,936 |
Row |
No |
Joe |
6 |
1,027,456 |
Page |
No |