深入非聚集索引:SQL Server索引进阶 Level 2
作者David Durant,2017/10/18(首次发布于:2014/11/26)
关于系列
本文属于进阶系列:Stairway to SQL Server Indexes
索引是数据库设计的基础,并告诉开发人员使用数据库关于设计者的意图。 不幸的是,当性能问题出现时,索引往往被添加为事后考虑。 这里最后是一个简单的系列文章,应该使他们快速地使任何数据库专业人员“快速”
SQL Server索引阶段1中的级别1通常引入了SQL Server索引,特别引入了非聚簇索引。作为我们的第一个案例研究,我们演示了从表中检索单个行时索引的潜在好处。在这个层面上,我们继续调查非集群指标。在超出从表中检索单个行的情况下,检查他们对良好查询性能的贡献。
就像大多数这些层面的情况一样,我们引入少量的理论,检查一些索引内部的内容来帮助解释理论,然后执行一些查询。这些查询是在没有索引的情况下执行的,并且打开了性能报告统计信息,以便查看索引的影响。
我们将使用我们在Level 1中使用的AdventureWorks数据库中的表的子集,集中在整个级别的Contact表。我们将只使用一个索引,即我们在1级中使用的FullName索引来说明我们的观点。为了确保我们控制Contact表上的索引,我们将在dbo模式中创建表的两个副本,并仅在其中一个上创建FullName索引。这将给我们我们的受控环境:表的两个副本:一个具有单个非聚集索引,另一个没有任何索引。
注意:
在这个楼梯级别显示的所有TSQL代码可以在文章底部下载。
清单1中的代码创建了Person.Contact表的副本,我们可以在我们希望以“clean slate”开始的任何时候重新运行这个批处理。
IF EXISTS ( SELECT * FROM sys.tables  WHERE OBJECT_ID = OBJECT_ID('dbo.Contacts_index')) DROP TABLE dbo.Contacts_index; GO IF EXISTS ( SELECT * FROM sys.tables  WHERE OBJECT_ID = OBJECT_ID('dbo.Contacts_noindex')) DROP TABLE dbo.Contacts_noindex; GO SELECT * INTO dbo.Contacts_index FROM Person.Contact; SELECT * INTO dbo.Contacts_noindex FROM Person.Contact;
清单2.1:制作Person.Contact表的副本
显示在这里的一个联系人表格片段:
ContactID FirstName MiddleName LastName EmailAddress . . 1288 Laura F Norman [email protected] 651 Michael Patten [email protected] 1652 Isabella R James [email protected] 1015 David R Campbell [email protected] 1379 Balagane Swaminath [email protected] 742 Steve Schmidt [email protected] 1743 Shannon C Guo [email protected] 1106 John Y Chen [email protected] 1470 Blaine Dockter [email protected] 833 Clarence R. Tatman [email protected] 1834 Heather M Wu [email protected] 1197 Denise H Smith [email protected] 560 Jennifer J. Maxham [email protected] 1561 Ido Ben-Sacha [email protected] 924 Becky R. Waters [email protected]
非聚集索引条目
以下语句在Contacts_index表上创建我们的FullName非聚簇索引。
CREATE INDEX FullName ON Contacts_index ( LastName, FirstName );
清单2.2 - 创建一个非聚集索引
请记住,非聚簇索引按顺序存储索引键,以及用于访问表中实际数据的书签。 您可以将书签看作一种指针。 未来的层次将更详细地描述书签,其形式和使用。
这里显示FullName索引的片段,包括姓氏和名字作为键列,加上书签:
:--- Search Key Columns : Bookmark . Russell Zachary => Ruth Andy => Ruth Andy => Ryan David => Ryan Justin => Sabella Deanna => Sackstede Lane => Sackstede Lane => Saddow Peter => Sai Cindy => Sai Kaitlin => Sai Manuel => Salah Tamer => Salanki Ajay => Salavaria Sharon =>
每个条目都包含索引键列和书签值。 另外,SQL Server非聚簇索引条目具有一些仅供内部使用的头信息,可能包含一些可选的数据值。 这两个都将在后面的层面进行讨论。 在这个时候,对非基本指标的基本理解也不重要。
现在,我们只需要知道键值就能使SQL Server找到合适的索引条目; 并且该条目的书签值使SQL Server能够访问表中相应的数据行。
索引条目的优点是在顺序
索引的条目按索引键值进行排序,所以SQL Server可以在任一方向上快速遍历条目。 顺序条目的扫描可以从索引的开始,索引的结尾或索引内的任何条目开始。
因此,如果一个请求要求所有以姓氏字母“S”开头的联系人(WHERE LastName LIKE'S%'),SQL Server可以快速导航到第一个“S”项(“Sabella,Deanna”), 然后遍历索引,使用书签访问行,直到到达第一个“T”条目; 在这一点上它知道它已经检索了所有的“S”条目。
如果所有选定的列都在索引中,上面的请求会更快地执行。 因此,如果我们发出:
SELECT FirstName, LastName FROM Contact WHERE LastName LIKE 'S%';
SQL Server可以快速导航到第一个“S”条目,然后遍历索引条目,忽略书签并直接从索引条目检索数据值,直到达到第一个“T”条目。在关系数据库术语中,索引已经“覆盖”了查询。
从序列数据中受益的任何SQL操作符都可以从索引中受益。这包括ORDER BY,GROUP BY,DISTINCT,UNION(不是UNION ALL)和JOIN ... ON。
例如,如果一个请求通过姓氏询问联系人的数量,SQL Server可以从第一个条目开始计数,然后沿索引继续。每次更改姓氏的值时,SQL Server都会输出当前计数并开始新的计数。与之前的请求一样,这是一个覆盖查询; SQL Server只访问索引,完全忽略表。
请注意按键列从左到右的顺序的重要性。如果一个请求询问所有姓“Ashton”的人,我们的索引是非常有用的,但是如果这个请求是针对所有名字是“Ashton”的人,那么这个索引几乎没有任何帮助。
测试一些样本查询
如果要执行后续的测试查询,请确保运行脚本以创建新的联系人表的两个版本:dbo.Contacts_index和dbo.Contacts_noindex; 并运行该脚本以在dbo.Contacts_index上创建LastName,FirstName索引。
为了验证上一节中的断言,我们打开了在1级中使用的相同性能统计信息,并运行一些查询; 有和没有索引。
SET STATISTICS io ON SET STATISTICS time ON
由于AdventureWorks数据库中的Contacts表中只有19972行,所以很难获得有意义的统计时间值。 我们大多数的查询会显示一个CPU时间值为0,所以我们不显示统计时间的输出; 只从统计数据IO中反映出可能需要读取的页数。 这些值将允许我们在相对意义上比较查询,以确定哪些查询具有哪些索引比其他索引执行得更好。 如果您想要更大的表进行更加实际的计时测试,则可以使用本文提供的构建百万行版本的Contact表的脚本。 接下来的所有讨论都假设你使用的是标准的19972行表。
测试涵盖的查询
我们的第一个查询是一个将被索引覆盖的查询; 一个为所有姓氏以“S”开头的联系人检索一组有限的列。 查询执行信息如表2.1所示。
SQL | SELECT FirstName, LastName FROM dbo.Contacts WHERE LastName LIKE 'S%' |
---|---|
没有索引 | (2130 row(s) affected) Table 'Contacts_noindex'. Scan count 1, logical reads 568. |
有索引 | (2130 row(s) affected) Table 'Contacts_index'. Scan count 1, logical reads 14. |
索引冲突 | IO reduced from 568 reads to 14 reads. |
评论 | 涵盖查询的索引是一件好事。 如果没有索引,则会扫描整个表以查找行。 “2130行”统计表明,“S”是姓氏的流行首字母,在所有联系人中占百分之十。 |
表2.1:运行覆盖查询时的执行结果
测试一个不包含的查询
接下来,我们修改我们的查询以请求与之前相同的行,但包括不在索引中的列。 查询执行信息见表2.2。
SQL | SELECT * FROM dbo.Contacts WHERE LastName LIKE 'S%' |
---|---|
没有索引 | 与以前的查询相同。 (因为它是一个表扫描)。 |
有索引 | (2130 row(s) affected) Table 'Contact_index'. Scan count 1, logical reads 568. |
索引冲突 | 没有冲突 |
评论 | 查询执行期间从未使用索引!SQL Server决定从一个索引条目跳转到表中对应的行2130次(每行一次)比扫描一百万行的整个表来查找它所需要的2130行更多的工作。 |
表2.2:运行非覆盖查询时的执行结果
测试一个不包含但更有选择性的查询
这一次,我们使我们的查询更具选择性; 也就是说,我们缩小了被请求的行数。 这增加了索引对该查询有利的可能性。 查询执行信息如表2.3所示。
SQL | SELECT * FROM dbo.Contacts WHERE LastName LIKE 'Ste%' |
---|---|
没有索引 | 与以前的查询相同。 (因为它是一个表扫描)。 |
有索引 | (107 row(s) affected) Table 'Contact_index'. Scan count 1, logical reads 111. |
索引冲突 | IO reduced from 568 reads to 111 reads. |
评论 | SQL Server访问107“Ste%”条目,所有这些条目都位于索引内连续。然后使用每个条目的书签来检索到对应的行。行不在表格内连续排列。该索引有利于此查询;但并不像第一个查询,“覆盖”查询那样受益;特别是在检索每一行所需的IO数量方面。您可能预期读取107个索引条目加107行将需要107 + 107个读取。为什么只有111个读取需要将在较高的水平。目前,我们会说只有极少的读取被用来访问索引条目;大部分用于访问行。由于前一个请求2130行的查询没有从索引中受益,而这个请求107行的查询确实从索引中受益 - 你也许会想知道“转折点在哪里?”SQL Server决策背后的计算也将在未来的层面上进行讨论。 |
表2.3:运行更具选择性的非覆盖查询时的执行结果
测试涵盖的聚合查询
我们最后一个示例查询将是一个聚合查询; 这是一个涉及计数,合计,平均等的查询。 在这种情况下,这是一个查询,告诉我们在联系人表中名称重复的程度。
结果部分看起来像这样:
Steel Merrill 1 Steele Joan 1 Steele Laura 2 Steelman Shanay 1 Steen Heidi 2 Stefani Stefano 1 Steiner Alan 1
查询执行信息见表2.4。
SQL | SELECT LastName, FirstName, COUNT(*) as 'Contacts' FROM dbo.Contacts WHERE LastName LIKE 'Ste%' GROUP BY LastName, FirstName |
---|---|
没有索引 | 与以前的查询相同。 (因为它是一个表扫描)。 |
有索引 | (104 row(s) affected) Table 'Contacts_index'. Scan count 1, logical reads 4. |
索引冲突 | IO reduced from 568 reads to 4 reads. |
评论 | 查询所需的所有信息都在索引中; 并且它在计算计数的理想顺序中处于索引中。 所有的“姓氏以'Ste'开始”在索引内是连续的; 并在该组内,单个名字/姓氏值的所有条目将被组合在一起。不需要访问表格; 也不需要对中间结果进行排序。 同样,涵盖查询的索引是一件好事。 |
表2.4:运行覆盖聚合查询时的执行结果
测试未覆盖的聚合查询
如果我们改变查询来包含不在索引中的列,我们可以得到我们在表2.5中看到的性能结果。
SQL | SELECT LastName, FirstName, MiddleName, COUNT(*) as 'Contacts' FROM dbo.Contacts WHERE LastName LIKE 'Ste%' GROUP BY LastName, FirstName, MiddleName |
---|---|
没有索引 | 与以前的查询相同。(因为它是一个表扫描)。 |
有索引 | (105 row(s) affected) Table 'ContactLarge'. Scan count 1, logical reads 111. |
索引冲突 | IO reduced from 568 reads to 111 reads; same as the previous non-covered query |
评论 | 处理查询时完成的中间工作并不总是出现在统计信息中。使用内存或tempdb排序和合并数据的技术就是这样的例子。实际上,一个指数的好处可能会比统计数据显示的好。 |
表2.5:运行非覆盖聚合查询时的执行结果
结论
我们现在知道非聚集索引具有以下特征。非聚集索引:
是一组有序的条目。
基础表的每行有一个条目。
包含一个索引键和一个书签。
由您创建。
由SQL Server维护。
由SQL Server使用来尽量减少满足客户端请求所需的工作量。
我们已经看到了SQL Server可以单独满足索引请求的例子。有些则完全忽略了指标。还有一些是使用索引和表的组合。为此,我们通过更新在第一级开始时的陈述来关闭第二级。
当请求到达您的数据库时,SQL Server只有三种可能的方式来访问该语句所请求的数据:
只访问非聚集索引并避免访问表。这只能在索引包含查询请求的所有数据的情况下才有可能
使用索引键访问非聚簇索引,然后使用选定的书签访问表的各个行。
忽略非聚簇索引并扫描表中的请求行。
一般来说,第一个是理想的;第二个比第三个好。在即将到来的级别中,我们将展示如何提高索引覆盖广受欢迎的查询的可能性,以及如何确定您的非覆盖查询是否具有足够的选择性以从您的索引中受益。但是,这将需要比我们尚未提出的更详细的索引内部结构信息。
在我们达到这一点之前,我们需要介绍另一种SQL Server索引;聚集索引。这是3级的主题。
下载代码
Resources:
Level 2 - NonClustered.sql | Level2_MillionRowContactTable.sql