You are on page 1of 12

Clustered indexes and primary keys

The company I work is for is implementing a custom system, built in Visual Studio and running
against SQL Server 2, to replace some old mainframe applications! It"s an all new database
design that will meet all of the old application data re#uirements and a whole slew of new ones! I
was responsible for the initial design, which I completed and handed over to a development team!
I had been working #uite closely with them for some time on the application, but I got pulled in
other directions and the app and database had proceeded without me! $ne day, the development
team raised a concern that the database was far too slow to support the anticipated transaction
load! I began my investigation!
The problem
The application in #uestion has a few data re#uirements that are different than the average
application! %asically, at its heart, it is a document management system! &owever, every change
to large sections of the data has to be stored historically, so that we have multiple versions of
pieces of a given document! 'ot all of the data stored is recreated for each version! (e needed
to know that change ")" was included in version "*", but that change "%" was in version "+" and, on a
completely different piece of data, read another table, and know that change "Q" was included in
version "2"! The data looks something like this,
Table 1
Version Value
* ")"
2
+ "%"
Table 2
Version Value
* "Some other value"
2 "Q"
+
(hen #ueries are run, the latest data in Table * is "%" from version three and the latest data in
Table 2 is "Q" from version 2!
The original design: clustered indexes and non-clustered PKs
The original database design started with a table, Document! -ach .ocument had one or more
Versions! The attributes that describe a .ocument were attached as children to the Version!
Since we knew that data access would always be by .ocument and Version, I made the decision
to denormali/e the data a bit and keep the DocumentId and VersionId on each table! This is a
sample of what the database would have looked like! 'ames have been changed and the
structure simplified for the purposes of the article!
)s you can see, my original design, which I refer to as the 0switch1board0 approach, placed a
clustered inde2 on each table on the .ocumentId and VersionId in order to group the data for the
anticipated access path! This means that in addition to the non1clustered primary key on each
table, there is a clustered inde2!
$r, at least this was how it was supposed to be!
The actual design
(hat I found was that, after several months on their own, and with input from some other .%)s,
the developers had dropped some of the inde2es, changed some of the primary keys to be
clustered and the inde2es to be non1clustered, removed some of the denormali/ation, and so on!
In other words, a total mess!
3y initial reaction, after cursing and spitting, was to simply clean up the database and put the
developers back on track! It wasn"t that simple! .uring this same period we had a consultant from
3icrosoft helping out in the database area! &e had been talking to the developers about their
problems and had proposed, directly to the development lead, a radical change to the design of
the database! 'ow I was presented with more than one solution to the problem!
The proposed new design: compound, clustered PKs
-veryone agreed that the e2isting database design wouldn"t support the re#uired user load, the
e2pected transaction volume, or the amount of data needed! .espite my inclination to revert to
my original design, I couldn"t simply dismiss the consultant"s idea, which was as follows, drop all
the inde2es, make the primary keys on all tables the clustered inde2 for that table and finally
redesign those keys so that each parent table acted as an identifier for the child table, thus
creating compound primary keys! &is proposed new design can be summari/ed as follows,
'$T-, The real design, being #uite a bit more comple2, had some tables with a clustered
compound primary key that contained as many as eight fields! 4olors and symbols are from
-5Studio! %lue represents foreign keys, 5ed are identifying foreign keys!
This design bothered me! It violated one of the fundamentals that I"d learned and read about for
years6 namely keeping the primary key small and narrow! It also looked like it would be difficult to
maintain! 7inally, after arguing back and forth about the merits and drawbacks of each of the
designs, we decided that testing them was the only way to be sure!
mplementing and testing the designs
In all, I had to implement and test three different designs,
*! The current, 0messed up0 design 8used as our baseline case9!
2! 3y original 0switch1board0 design, as described in 7igure * 8produced by cleaning up the
current 0messed up0 design9!
+! The proposed new 04ompound :; design, as described in figure 2!
mplementing the designs
&aving created each basic design, I needed to load each with the various defined data loads
under which we wished to test! I also had to modify the stored procedures that ran the #ueries
against these designs, as appropriate!
!oading the data
I needed to test under multiple data loads, pro<ecting two, four, and eight years worth of data, and
therefore create multiple databases, for each of the three designs! 4reation of these data loads
was simple in concept but #uite labor1intensive! I had a known client list from which to pull a data
distribution that would accurately represent the real business data and provide us with some of
the largest anticipated data sets! 7urther, by loading from known data, it made it possible to
define our test transactions based on that same known data! In other words, I knew which
organi/ations we would be calling and could make that list known to the testing tool! In order to
facilitate the data load process, I took advantage of a tool from the Quest suite called Data
Factory! I first defined a two year load, verified that it was working correctly, backed up the
database, and then simply doubled the initial row counts across the tables in order to get to the
four and then eight year data loads! -ach went into a separate database! In the end, I had nine
different databases representing the three designs to be tested!
Veri"ying the stored procedures
'e2t, I had to modify and verify the stored procedures to work with the 0cleaned up0 original
design and the new 4ompound :; design 8obviously, the procedures for the e2isting 0messed
up0 design I simply left alone9! The stored procedures consisted of a set of simple, re#uired
#$%&T, 'PD(T% and $%!%CT commands plus some more comple2 $%!%CT statements
needed to retrieve the latest values across the historical data, as described earlier! The cleaned
up version re#uired me to check a number of stored procedures and rewrite #uite a few of them!
The new 0compound :;0 architecture re#uired me to completely rewrite the <oin criteria on all the
procedures! I made the initial pass through this and then had the 3S consultant clean up my work
so that his design was represented in the best possible light! I wanted it to fail, but I wanted it to
fail fairly!
-ven in the original design the #ueries to retrieve the latest values across the historical, versioned
data were rather cumbersome to write so we had spent a lot of time on getting them <ust right!
The original #ueries, with only the switchboard style of primary1to1foreign key relationships,
looked something like this,
S-L-4T =
75$3 dbo!.ocument d
I''-5 >$I' dbo!Version v
$' d!.ocumentId ? v!.ocumentId
L-7T $@T-5 >$I' dbo!.ocument4ountry dc
$' d!.ocumentId ? dc!.ocumentId
)'. dc!VersionId ? 8S-L-4T 3)A8dc2!VersionId9
75$3 dbo!.ocument4ountry dc2
(&-5- dc2!VersionId B? v!VersionId
)'. dc2!.ocumentId ? d!.ocumentId
9
I''-5 >$I' dbo!.ocumentLimit dl
$' d!.ocumentId ? dl!.ocumentId
)'. dl!VersionId ? 8S-L-4T 3)A8dl2!VersionId9
75$3 dbo!.ocumentLimit dl2
(&-5- dl2!VersionId B? v!VersionId
)'. dl2!.ocumentId ? d!.ocumentId
9
I''-5 >$I' dbo!LimitQualifier l#
$' l#!.ocumentLimitId ? dl!.ocumentLimitId
)'. d!.ocumentId ? l#!.ocumentId
)'. l#!VersionId ? 8S-L-4T 3)A8dl2!VersionId9
75$3 dbo!LimitQualifier l#2
(&-5- l#2!VersionId B? v!VersionId
)'. l#2!.ocumentId ? d!.ocumentId
9
These #ueries would get the latest value from each of the separate tables using the 3)A
function!
'$T-, (e e2perimented with using T$:8*9 instead of 3)A, and in some situations it
performed better! &owever, use of the aggregate function gave a more consistent
performance across the system, so we standardi/ed on it!
4learly, as the number of table <oins grows, these #ueries can become #uite unwieldy! &owever,
the new compound :; architecture produced different and even more unwieldy #ueries,
S-L-4T =
75$3 dbo!.ocument d
I''-5 >$I' dbo!Version v
$' d!.ocumentId ? v!.ocumentId
L-7T $@T-5 >$I' dbo!.ocument4ountry dc
$' v!.ocumentId ? dc!.ocumentId
)'. dc!VersionId ? 8S-L-4T 3)A8dc2!VersionId9
75$3 dbo!.ocument4ountry dc2
(&-5- dc2!VersionId B? v!VersionId
)'. dc2!.ocumentId ? v!.ocumentId
9
I''-5 >$I' dbo!.ocumentLimit dl
$' v!.ocumentId ? dl!.ocumentId
)'. dl!VersionId ? 8S-L-4T 3)A8dl2!VersionId9
75$3 dbo!.ocumentLimit dl2
(&-5- dl2!VersionId B? v!VersionId
)'. dl2!.ocumentId ? v!.ocumentId
9
I''-5 >$I' dbo!LimitQualifier l#
$' dl!.ocumentId ? l#!.ocumentId
)'. l#!VersionId ? 8S-L-4T 3)A8dl2!VersionId9
75$3 dbo!LimitQualifier l#2
(&-5- l#2!VersionId B? v!VersionId
)'. l#2!.ocumentId ? v!.ocumentId
9
)'. dl!LimitTypeId ? l#!LimitTypeId
In order to keep the article manageable, and because I can"t show the real design, these #ueries
seem almost as simple as for the original design! &owever, trust me, as the structure gets deeper
and more comple2, some of the <oins are between si2 and eight columns wide rather than the
currently displayed ma2imum of four! This causes not only the main part of the #uery to grow, but
the sub#ueries to get e2tended as well!
Testing the designs
In order to test which design would facilitate the fastest #ueries and function in the most scalable
way, I had to come up with a method of performing database1only testing! This was the only way
to eliminate any concerns that we were seeing artifacts from the user interface or business
faCade, etc! )s noted, I needed to test each design under three different data loads and I also
needed to ramp up the simultaneous user load in order to test the scalability and measure the
breakdown point of each design, for each data load! The tool that answered all these needs was
Quest"s Benchmark Factory!
I had to create a set of transactions to run the re#uired #ueries against each of the database
designs! .ue to a limitation in %enchmark 7actory 8it can"t handle output parameters in SQL
Server9 I was forced to create a wrapper procedure that encapsulated the re#uired series of
reads, writes, udpates, and finally a delete, that represented how the application would be making
the database calls! This wasn"t the optimal way for the database to perform since the #ueries
were running within a single transaction rather than as a series of transactions and therefore not
the best way to run the tests! &owever, as long as I did the same thing to each of the database
designs, I"d be comparing apples1to1apples!
Test results
I set up the tests to run for + minutes at each of a variety of concurrent user loads6 2, D, E, *F, 2
82 being the ma2 for our license9! This meant 2!G hours at a pop for each test! (hat with
reseting the databases between tests and gathering up all the test data, I could only complete two
tests a day! @ltimately, it took most of a week to hack through all this stuff, on top of the three
weeks it took to prepare it all! &owever, in the end, I had some answers!
%xisting Design )baseline case*
The first tests were run against the e2isting design, messed up inde2es and everything else that
was wrong with it! It was hardly a shock to see that it didn"t perform very well! The results for a
two year data load showed an incredibly slow performance, even when one takes into account
that the 0transaction0 displayed represents the wrapper stored procedure and not the e2ecution of
each of the individual procedures within the wrapper!
'serload TP$ %rrors (+g Time ,in Time ,ax Time
2 *!2* *!FGH !FI* 2!G*+
D *!* +!HG2 *!+ G!+D
E *!* I!EE2 2!EG2 **!2+H
*F *!+H **!GD* D!EE2 2D!E2F
2 !H+ 2*!G2H I!HD 2E!+G+
Things went from bad to worse! The four year data load started deadlocking immediately! I tried
resetting the database, and even SQL Server, to clear up the problem, but nothing worked! I tried
running the eight year data load and got a partial set of results before the deadlocks started
again,
'serload TP$ %rrors (+g Time ,in Time ,ax Time
2 *!I ** *!EFD *!EI F!+EG
D !II +HH G!*I !GFH 2*!+
NOTE: When the information collected was reviewed we fo!nd that we were "ettin" lots of table
scans inde# scans and lock escalation all of which were contrib!tin" to the deadlocks!
Cleaned up -switch-board- design
I believed very strongly that the cleaned up inde2es and primary keys would work well enough!
The tests supported this #uite strongly, as you can see for the two year data load case,
'serload TP$ %rrors (+g Time ,in Time ,ax Time
2 2!D !E++ !**G 2!HEG
D D!22 !HDI !EE +!2+H
E H!HH !E !D +!HH
*F 2+!IH !FI2 !F E!E2*
2 2+!2F !EGH !*+F H!F
(hen compared to the baseline, I got a very consistent level of performance, with the
transactions completing in an average time of between !F and !H seconds! The design
produced minimum responses times down to !D s 8compared to around !I s for the e2isting
design9! The ma2imum response time never e2ceeded H s, whereas for the e2isting design it
e2ceeded 2E s for a single transaction!
I approached the test for the to the four year data set with some degree of trepidation, since the
e2isting design had failed so spectacularly! I shouldn"t have worried,
'serload TP$ %rrors (+g Time ,in Time ,ax Time
2 2!+E !ED !+DG 2!FHD
D D!I+ !EDD !DH 2!FHI
E H!E !E*F !D2 +!+2H
*F 22!HF !FHE !IF **!G+2
2 22!2 !H2 !*E *D!*HF
This showed that, despite doubling the amount of data in the system, not only did I avoid
deadlocks, but I got results that were entirely consistent with the two year data test, including that
odd sweet spot at *F users! The ma2imum times were a bit higher, but not alarmingly so! I went
on to the eight year test feeling more optimistic,
'serload TP$ %rrors (+g Time ,in Time ,ax Time
2 2!DD !E*E !*DH 2!FH
D D!ED !E2G !F+ 2!E+E
E H!HE !E* !D2 +!GD*
*F 2+!2 !FHD !I *!2+G
2 22!2+ !EHH !H2 *!*++
The results were the same! @se of two, four and eight years worth of data didn"t cause a
significant change in performance! I had captured :erf3on data as well and it showed how the
load scaled as more users were added! This is from the eight1year test,
)s you can see, as the number of users increased, 4:@ J disk IK$ went up accordingly! The
interesting moment occurs when going from eight to si2teen simultaneous users! The 4:@ goes
#uite high, but the disk #ueuing goes through the roof 8more on this later9!
If I hadn"t had the consultant"s design in hand, I would have declared the cleaned up design a
success and walked away #uite satisfied! %ut, I had to run the same tests against this new
design!
The compound PK design
3y initial estimate was that this new design might be about *12L faster, which #uite honestly
wouldn"t <ustify the additional overhead of rewriting all the stored procedures with e2tra code, and
so on! The two year data tests came back,
'serload TP$ %rrors (+g Time ,in Time ,ax Time
2 *H!*G !*D !+E 2!H*
D +!G !*+ !+E 2!+GD
E +!+H !2F2 !G I!*G
*F 2F!H !GHD !*2 E!*2
2 2I!* !ID !H **!D*2
I was a little surprised! $;, more than a little surprised! )t the lower end, 21E users, this was
running between four and eight times faster! &owever, for the higher user loads the performance
was only *12L better M in line with my original e2pectations!
Something was going on here that merited a lot more investigation! I didn"t believe we"d hit
deadlocks above E users, but given the failure of the baseline case at the four year data load, I
wasn"t sure,
'serload TP$ %rrors (+g Time ,in Time ,ax Time
2 2!+E !HE !+F *!H++
D 2H!+2 !*+F !+E 2!*G
E 2H!22 !2IF !G E!DDI
*F 2E!GI !GF2 !E **!HE+
2 2I!ID !I2* !H **!IH
The results were again consistent with those obtained for the two year data load, with the same
performance trend as for the cleaned up design, but this time with a slight drop1off in T:S
performance at si2teen users! 'othing for it but to get the eight year data load results,
'serload TP$ %rrors (+g Time ,in Time ,ax Time
2 *G!HH !*2D !+E 2!D
D 2I!GG !*DG !+E 2!IEH
E 2H!2H !2IF !DD F!I2+
*F 2H!E+ !G+F !IE E!E+D
2 2H!*D !FEI !*+ E!FHG
These were also very consistent with the other results! :erformance was decidedly better at low
user loads, but only somewhat better at higher loads! The :erfmon data gave a better indication
of why things were behaving as they were,
)s you can see, disc #ueuing was #uite variable, but on average, much lower than for the
cleaned up design! It was the 4:@ that was getting the most use in this design! The strange
decline in performance at *F users directly corresponded to the 4:@ ma2ing out at about that
value! 4learly, as 4:@"s were added or upgraded, this design would be faster than the original!
The casue of this radical increase in performance was found in the #uery plans!
The .uery plans
)t the conclusion of the testing, a few things were very clear! The e2isting design was not only
slow, but missing or improper clustered inde2es were leading to table scans and unnecessary
lock escalation, resulting in deadlocks! (e could get ade#uate performance by simply cleaning
up the inde2es and stored procedures as they e2isted! %ut, when you can absolutely point to a
four fold increase in performance, you have to assume that it"s worth the time and effort to
achieve it! The only #uestion remaining was, why did the new design outperform the old oneN
The answer was provided by the respective #uery plans! In my switch1board design, the clustered
inde2es were being used #uite well! &owever, because the primary key structure was
independent of the clustered inde2, we were seeing a bookmark lookup from the clustered inde2
to the primary key! This radically increases the disk IK$ 8for a more thorough e2amination see, for
e2ample, 5andy .yess" article on SQL Server 4entral,
&owever, the compound primary key structure eliminated this processing! 3y original design
re#uired, at a minimum two inde2es, the cluster and the primary key! The new design reduced
this to one, which in turn reduced the actual si/e of the database noticeably! This is where the
potential for added costs during inserts due to the wider key structures actually ends up in a
performance improvement since only a single inde2 is being maintained! (e even saw, in some
cases, the elimination of a clustered inde2 scan because the more selective inde2 created by
adding multiple columns was able to find the data in more efficiently!
Conclusions
(e decided to go with the compound key approach! It does add to the number of lines of code
and every time we do a review with a different .%), the si/e of the primary key"s will start them
twitching! That said, it"s scaling very well and looks to be a success! ) concern was raised that
the larger and more comple2 #ueries would compile slowly, but testing again assured us that,
while it was a bit slower, it was easily offset by the performance gains! (e"re now evaluating all
new pro<ects as they come on board to determine if they need #uick and easy development 8the
switch1board approach9, or reliable speed and scalability, the compound key approach! $h, and
we"ve implemented more fre#uent reviews of databases that we"ve turned over to development to
ensure they are not deviating from the original design as far as this one did!
OOO
$rant Fritchey is a database administrator for a ma%or ins!rance com&any with 1' years
e#&erience in (T) *e has been workin" with +,- +erver since .)/ back in 1001 and has also
develo&ed in 2B 2B)Net 34 and 5ava) *e is c!rrently workin" on methods for incor&oratin"
6"ile develo&ment techni7!es into database desi"n and develo&ment at o!r com&any)

You might also like