All of the code in this section uses the Ch7Examples.sql code file.
This section looks at a
couple of examples demonstrating contention issues involving latches
and spinlocks. Some of these examples are borrowed from various
presentations involving the SQLCAT team at SQLBits events in the U.K.,
and we are indebted to Thomas Kejser in particular for his work on
these.
Inserts When the Clustered Index Key Is an Identity Field
A lot of advice suggests using an
identity field for the clustered index on a table. Certainly there are
benefits to doing this. An identity field is typically an int or bigint
type, making it relatively small compared to some other candidates for
primary keys, in particular uniqueidentifier
fields, which can cause frequent page splits, as well as being overly
large, especially because clustered index keys appear in nonclustered
indexes as well.
However, for tables that use identity fields for
clustered index keys, when the number of inserts scales up, the final
page will become “hot,” and contention could occur.
Consider the scenario in which a lot of processor
cores are trying to insert data into the same page. The first session
to reach the page in question will obtain a PAGELATCH_EX latch; but in the same moment, a large number of other threads might also be trying to acquire a PAGELATCH_EX latch. There would also be PAGELATCH_SH
latches acquired at the higher index levels, to allow these pages to be
traversed. If the insert needs to tip onto a new page, then a PAGELATCH_EX would be required at the next index level higher.
If sys.dm_os_waiting_tasks were queried during heavy inserts, it would likely show PAGELATCH_EX waits, with the resource_description column showing the page of note. The page could be examined, with DBCC PAGE, and identified as the table under stress.
The point here is not to make a case against ever
using an identity field for a clustered index. In many systems, it’s
still an excellent idea. However, if you’re seeing a large amount of
latch contention during busy periods of insertion into such a table,
then this design choice may certainly be a contributor to the
predicament.
The solution has to move the activity away from
the hotspot of insertion. While this could be done by simply replacing
the identity field with a new uniqueidentifier field, populated with newid()
values, the same goal can be achieved in other ways. One way of
spreading the load sufficiently without losing the benefits of having a
small clustered index, with the data nicely arranged in a b-tree, is to
introduce partitioning. This way, the table is spread across a number
of b-tree structures, instead of just one. With a bit of planning, the
activity can be spread across the partitions. There may still be a
hotspot for each partition, but this could well be enough to relieve
the stress on the problem page.
The following example assumes that eight
partitions are wanted, but you could choose whatever number suited your
needs. All the partitions can be put on the same filegroup; this
exercise is not designed to use partitions to spread the table across
multiple filegroups, but merely to make additional b-tree structures to
store the table.
CREATE PARTITION FUNCTION pf_spread (TINYNT) AS RANGE LEFT FOR VALUES
(0,1,2,3,4,5,6);
CREATE PARTITION SCHEME ps_spread AS PARTITION pf_spread ALL TO (PRIMARY);
To spread the data across your various
partitions, you simply need to introduce into the table a column that
causes the data to be distributed. In this case, ID % 8 will do nicely:
ALTER TABLE MyStressedTable
ADD PartID AS CAST(ID % 8 AS TINYINT) PERSISTED NOT NULL;
Once this is done, the clustered index simply needs to be created on the partitions:
CREATE UNIQUE CLUSTERED INDEX cixMyStressedTable (ID, PartID) ON ps_spread(PartID);
Now, inserts will be cycled around the
eight partitions, which should enable many more inserts to be done
before latch contention occurs. Going back to the analogy using the
chairs at a party, this partitioning provides seven more chairs. If the
number of threads being used to perform the inserts is such that there
is now a very small number of threads per b-tree, then the likelihood
of contention is very much reduced.
Of course, additional partitions might translate into more work finding data using the ID field. A query that simply filters on the ID
field would need to search all eight partitions, despite the fact that
you can see a correlation between the ID and the partition. To avoid
having to search across all the partitions, code such as
SELECT *
FROM dbo.MyStressedTable
WHERE ID = @id;
should be changed to
SELECT *
FROM dbo.MyStressedTable
WHERE ID = @id
AND PartID = CAST(@id % 8 AS TINYINT);