What you're doing is you're using a table as a queue. Your update is the dequeue method. But the clustered index on the table is a poor choice for a queue. Using tables as Queues actually impose quite stringent requirements on the table design. Your clustered index must be the dequeue order, in this case likely ([DataType], [DataStatus], [ProcessDate])
. You can implement the primary key as a nonclustered constraint. Drop the non-clustered index Idx
, as the clustered key takes its role.
Another important piece of the puzzle is to keep the row size constant during processing. You have declared the ProcessThreadId
as a VARCHAR(100)
which implies the row grows and shrinks as is being 'processed' because the field value changes from NULL to non-null. This grow-and-shrink pattern on the row causes page splits and fragmentation. I can't possibly imagine a thread ID that is 'VARCHAR(100)'. Use a fixed length type, perhaps an INT
.
As a side note, you do not need to dequeue in two steps (UPDATE followed by SELECT). You can use OUTPUT clause, as explained in the article linked above:
/**************************************/
CREATE TABLE [Table]
(
[PrimaryKeyId] [INT] IDENTITY(1,1) NOT NULL PRIMARY KEY NONCLUSTERED,
[ForeignKeyId] [INT] NOT NULL,
/* more columns ... */
[DataType] [CHAR](1) NOT NULL,
[DataStatus] [DATETIME] NULL,
[ProcessDate] [DATETIME] NOT NULL,
[ProcessThreadId] INT NULL
);
CREATE CLUSTERED INDEX [Cdx] ON [Table]
(
[DataType],
[DataStatus],
[ProcessDate]
);
/**************************************/
declare @BatchSize int, @ProcessThreadId int;
/**************************************/
WITH cte AS (
SELECT TOP (@BatchSize) [PrimaryKeyId], [ProcessThreadId] , ... more columns
FROM [Table] WITH ( ROWLOCK, UPDLOCK, READPAST )
WHERE [DataType] = 'X'
AND [DataStatus] IS NULL
AND [ProcessDate] < DATEADD(m, -2, GETDATE()) -- older than 2 months
AND [ProcessThreadId] IS NULL
)
UPDATE cte
SET [ProcessThreadId] = @ProcessThreadId
OUTPUT DELETED.[PrimaryKeyId] , ... more columns ;
/**************************************/
In addition I would consider moving successfully processed items into a different, archive, table. You want your queue tables to hover near zero size, you do no want them to grow as they retain 'history' from unneeded old entries. You may also consider partitioning by [ProcessDate]
as an alternative (ie. one current active partition that acts as the queue and stores entries with NULL ProcessDate, and another partition for everything non-null. Or multiple partitions for non-null if you want to implement efficient deletes (switch out) for data that has passed the mandated retention period. If things get hot you can partition in addition by [DataType]
if it has enough selectivity, but that design would be really complicated as it requires partitioning by persisted computed column (a composite column that glues together [DataType] and [ProcessingDate]).
I think the problem here is a difference in terminology.
The "number of writes" that's usually referred to is the number of object accesses, rather than the number of pages that get touched by the physical operation.
The reason why that's usually used as a metric in discussion is because it's a more "stable" and meaningful number to talk about. As we're getting into here, the number of pages touched by an INSERT
statement for even a single row depends on many factors, so it's not a very useful quantity outside your own environment and situation.
The one thing I would pick at from the article quote is this (emphasis mine):
One write for inserting the row, and one write for updating the non-clustered index.
This may be confusing. Inserting a row into the base table would involve an insert to the base table, and also an insert into each nonclustered index (ignoring special index features), not an update.
So if a record has to be updated, say the value 1 has to be updated to 7, won't the update need to be applied to both the key in the clustered index top node (this may, in cases, cause a re-structuring of the entire structure) and the corresponding value in the record in the leaf-page?
Yes, assuming the column that was updated is in the index key. However, this is still a single object access, and hence a "single write."
Best Answer
The "B" in B-tree doesn't necessarily stand for "balanced". It depends on who you ask. See for instance this. As often is the case, terminology degrades (over time).
All SQL Server "row indexes" (as opposed to other types of indexes in SQL Server like hash, columnstore, full-text, XML (to some extent) and geospatial) are B-tree indexes. They are always "balanced" in the sense that you have the same depth (number of pages) from the root to all leaf pages.
This has no relationship with any of the two types of fragmentation that we usually talk about in the SQL Server world: external (jump back and forth when following the linked list) or internal (non-full pages).
One can argue that a clustered index in SQL Server should be considered a B+ tree, since the leaf "has the data", but again I doubt that you will find and authoritative reference for how the terms are used.