{"id":122,"date":"2008-09-23T18:00:42","date_gmt":"2008-09-23T16:00:42","guid":{"rendered":"https:\/\/www.sqlinthewild.co.za\/?p=122"},"modified":"2008-09-23T18:00:42","modified_gmt":"2008-09-23T16:00:42","slug":"do-wide-indexes-slow-queries-down","status":"publish","type":"post","link":"https:\/\/www.sqlinthewild.co.za\/index.php\/2008\/09\/23\/do-wide-indexes-slow-queries-down\/","title":{"rendered":"Do wide indexes slow queries down?"},"content":{"rendered":"<p>I got asked this question during one of my TechEd sessions. It was a bit complex to answer there and then, so I answered by mail a few days later, and I thought it would make a good blog post if expanded a bit more.<\/p>\n<p>Assume a table with 5 columns (imaginatively called A, B, C, D and Z). A and B are ints, C is a datetime and D is a char(50). Let&#8217;s assume that Z is an int, identity, primary key and has the clustered index<\/p>\n<p>If I put an index on A and B, the size of the index key (ignoring headers and other overhead) is 8 bytes. Add in the clustering key for row location and each index leaf record in 12 bytes wide. That means that (at 100% fill factor) there are around 660 index rows per leaf page. Since I&#8217;m ignoring headers, this rough calc will not exactly match reality, but it&#8217;s close enough.<\/p>\n<p>If the table has 100000 rows, the the leaf level of the index consists of 152 pages.<\/p>\n<p>To calculate the number of pages above, one should know that for each leaf page, the next level higher has one index row in &#8211; the first value on the page. Hence, for 152 leaf pages, the level above will have 152 index rows as 12 bytes each totalling 1824 bytes. This will fit on one page, so the level above the leaf is the index root, and this index has only two levels.<\/p>\n<p>To seek a single row from this index hence requires 2 page reads.<\/p>\n<p><!--more-->Now, the clustered index has row sizes of around (again ignoring row headers) 70 bytes. At that size, there will be 115 rows per page, for a total of 870 pages. For the level above, only the clustering key needs to be in the pages (4 bytes) along with the page pointer (which is a binary(6)) and 870 index entries are required. At 10 bytes\/row that means that 2 pages are required at this level. That means that there is one more level to the index &#8211; the root, making the clustered index a three level index.<\/p>\n<p>To seek a single row from the clustered index hence requires 3 page reads.<\/p>\n<p>Let&#8217;s take a quick example query.<br \/>\nSELECT A,B FROM TestTable WHERE A=2 and B=16<br \/>\nLet&#8217;s further assume that this returns 100 rows, and that those 100 rows are situated on two neighbouring pages of the nonclustered index (worst case). To completely satisfy this query requires 3 page reads. 2 to find the beginning of the range and a third on the next page (index leaf level have next and previous page pointers). Very fast query, very few IOs.<\/p>\n<p>Now a second query<br \/>\nSELECT A,D FROM TestTable WHERE A=2 and B=16<\/p>\n<p>The index rows for this query are on the same 100 leaf pages of the nonclustered index. The problem is, the noncluster doesn&#8217;t contain D, and so a bookmark\/key lookup is required for each row. 100 key lookups at 3 reads each means that in total this query will take 303 page reads.<\/p>\n<p>What if we widened the nonclustered index? I&#8217;ll put D as an include column.<\/p>\n<p>Now, the leaf index rows are around 62 bytes in size, meaning that the nonclustered index has\u00a0 130 rows per page and a total of 770 leaf pages.<\/p>\n<p>The upper level of the index still only requires 12 bytes, and so 770 leaf pages requires 770 index entries at 12 bytes each is 9240 bytes, for a total of 2 pages. This means that another level of the index is required and so this nonclustered index is three levels deep and a single row seek requires 3 reads.<\/p>\n<p>Back to the two examples.<\/p>\n<p>SELECT A,B FROM TestTable WHERE A=2 and B=16<br \/>\nThis still returns 100 rows and those 100 rows are still across two index leaf pages (again assuming worst case). Now this query requires 4 reads, 3 to find the start of the range and 1 to get the next page<\/p>\n<p>SELECT A,D FROM TestTable WHERE A=2 and B=16<br \/>\nThis also still returns 100 rows and those 100 rows are also still across two index leaf pages. The difference now is that D is contained within the index and so no bookmark\/key lookup is required. This query also requires only 4 page reads, 3 to find the start and 1 to get the next page.<\/p>\n<p>So, the width of the index rows has more than quadrupled, and the queries that use index seeks only require one more read.<\/p>\n<p>The theory looks nice, let&#8217;s see if reality agrees with it.<\/p>\n<p>Create Table TestingWideIndexes (<br \/>\nA int not null,<br \/>\nB int not null,<br \/>\nC datetime not null,<br \/>\nD char(50) not null,<br \/>\nZ int identity primary key &#8212; defaults to clustered<br \/>\n)<br \/>\nGO<\/p>\n<p>Insert into TestingWideIndexes (A,B,C,D)<br \/>\nSELECT ID\/15, FLOOR((RAND(ID*1422)*10)), DATEADD(hh, ID, &#8216;1990\/01\/01&#8217;), &#8216;x&#8217; FROM (<br \/>\nSELECT top 100000 a.number*100+b.number AS ID<br \/>\nfrom master..spt_values a cross join master..spt_values b<br \/>\nwhere a.name is null and b.name is null) x<\/p>\n<p>CREATE INDEX Testing_1 ON TestingWideIndexes (A, B)<\/p>\n<p>SELECT * from sys.dm_db_index_physical_stats(db_id(), object_id(&#8216;TestingWideIndexes&#8217;), 4, 0, DEFAULT )<br \/>\n&#8212; page count 186<\/p>\n<p>SET STATISTICS IO ON<br \/>\nGO<\/p>\n<p>SELECT A,B FROM TestingWideIndexes WHERE A=180 and B=3<br \/>\n&#8212; Table &#8216;TestingWideIndexes&#8217;. Scan count 1, logical reads 2<br \/>\n&#8212; SQL Server Execution Times:<br \/>\n&#8212;\u00a0\u00a0 CPU time = 0 ms,\u00a0 elapsed time = 0 ms.<\/p>\n<p>SELECT A,D FROM TestingWideIndexes WHERE A=180 and B=3<br \/>\n&#8212; Table &#8216;TestingWideIndexes&#8217;. Scan count 1, logical reads 268<br \/>\n&#8211;SQL Server Execution Times:<br \/>\n&#8212;\u00a0\u00a0 CPU time = 16 ms,\u00a0 elapsed time = 0 ms.<\/p>\n<p>DROP INDEX Testing_1 ON TestingWideIndexes<br \/>\nCREATE INDEX Testing_2 ON TestingWideIndexes (A, B) INCLUDE (D)<\/p>\n<p>SELECT A,B FROM TestingWideIndexes WHERE A=180 and B=3<br \/>\n&#8212; Table &#8216;TestingWideIndexes&#8217;. Scan count 1, logical reads 3<br \/>\n&#8212; SQL Server Execution Times:<br \/>\n&#8212;\u00a0\u00a0 CPU time = 0 ms,\u00a0 elapsed time = 0 ms.<\/p>\n<p>SELECT A,D FROM TestingWideIndexes WHERE A=180 and B=3<br \/>\n&#8212; Table &#8216;TestingWideIndexes&#8217;. Scan count 1, logical reads 3<br \/>\n&#8212; SQL Server Execution Times:<br \/>\n&#8212;\u00a0\u00a0 CPU time = 0 ms,\u00a0 elapsed time = 0 ms.<\/p>\n<p>So to answer the question, the wider index hasn&#8217;t slowed the query down, it&#8217;s made it faster.<\/p>\n<p>All well and good. What about scans though? With more index leaf pages, they&#8217;ll require more IOs and should be slower.<\/p>\n<p>&#8212; with both indexes created<br \/>\nselect A, B from TestingWideIndexes WITH (Index = Testing_1)<br \/>\nselect A, B from TestingWideIndexes WITH (Index = Testing_2)<\/p>\n<p>Results:<\/p>\n<p>Table &#8216;TestingWideIndexes&#8217;. Scan count 1, logical reads 188<br \/>\nSQL Server Execution Times:<br \/>\nCPU time = 32 ms,\u00a0 elapsed time = 15141 ms.<\/p>\n<p>Table &#8216;TestingWideIndexes&#8217;. Scan count 1, logical reads 813<br \/>\nSQL Server Execution Times:<br \/>\nCPU time = 31 ms,\u00a0 elapsed time = 15660 ms.<\/p>\n<p>I ran it several times and the CPU times varied on each run, sometimes the wide index had a higher CPU time, sometimes the narrow one did. They were always very close. So again here, the wider index has not slowed the query down.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>I got asked this question during one of my TechEd sessions. It was a bit complex to answer there and then, so I answered by mail a few days later, and I thought it would make a good blog post&#8230; <a class=\"read-more-button\" href=\"https:\/\/www.sqlinthewild.co.za\/index.php\/2008\/09\/23\/do-wide-indexes-slow-queries-down\/\">(Read more)<\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"jetpack_post_was_ever_published":false,"_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":false,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_jetpack_memberships_contains_paid_content":false,"footnotes":"","jetpack_publicize_message":"","jetpack_publicize_feature_enabled":true,"jetpack_social_post_already_shared":false,"jetpack_social_options":{"image_generator_settings":{"template":"highway","default_image_id":0,"font":"","enabled":false},"version":2}},"categories":[24,15],"tags":[],"class_list":["post-122","post","type-post","status-publish","format-standard","hentry","category-indexes","category-sql-server"],"jetpack_publicize_connections":[],"jetpack_featured_media_url":"","jetpack_shortlink":"https:\/\/wp.me\/p7h6n-1Y","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/www.sqlinthewild.co.za\/index.php\/wp-json\/wp\/v2\/posts\/122","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.sqlinthewild.co.za\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.sqlinthewild.co.za\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.sqlinthewild.co.za\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.sqlinthewild.co.za\/index.php\/wp-json\/wp\/v2\/comments?post=122"}],"version-history":[{"count":0,"href":"https:\/\/www.sqlinthewild.co.za\/index.php\/wp-json\/wp\/v2\/posts\/122\/revisions"}],"wp:attachment":[{"href":"https:\/\/www.sqlinthewild.co.za\/index.php\/wp-json\/wp\/v2\/media?parent=122"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.sqlinthewild.co.za\/index.php\/wp-json\/wp\/v2\/categories?post=122"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.sqlinthewild.co.za\/index.php\/wp-json\/wp\/v2\/tags?post=122"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}