做者David Durant,2017/10/18(首次发布于:2014/11/26)sql
本文属于进阶系列:Stairway to SQL Server Indexes数据库
索引是数据库设计的基础,并告诉开发人员使用数据库关于设计者的意图。 不幸的是,当性能问题出现时,索引每每被添加为过后考虑。 这里最后是一个简单的系列文章,应该使他们快速地使任何数据库专业人员“快速”数据库设计
SQL Server索引阶段1中的级别1一般引入了SQL Server索引,特别引入了非聚簇索引。做为咱们的第一个案例研究,咱们演示了从表中检索单个行时索引的潜在好处。在这个层面上,咱们继续调查非集群指标。在超出从表中检索单个行的状况下,检查他们对良好查询性能的贡献。sqlserver
就像大多数这些层面的状况同样,咱们引入少许的理论,检查一些索引内部的内容来帮助解释理论,而后执行一些查询。这些查询是在没有索引的状况下执行的,而且打开了性能报告统计信息,以便查看索引的影响。性能
咱们将使用咱们在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表的副本code
显示在这里的一个联系人表格片断:orm
ContactID FirstName MiddleName LastName EmailAddress . . 1288 Laura F Norman laura1@adventure-works.com 651 Michael Patten michael20@adventure-works.com 1652 Isabella R James isabella6@adventure-works.com 1015 David R Campbell david8@adventure-works.com 1379 Balagane Swaminath balaganesan0@adventure-works.c 742 Steve Schmidt steve3@adventure-works.com 1743 Shannon C Guo shannon16@adventure-works.com 1106 John Y Chen john2@adventure-works.com 1470 Blaine Dockter blaine1@adventure-works.com 833 Clarence R. Tatman clarence0@adventure-works.com 1834 Heather M Wu heather6@adventure-works.com 1197 Denise H Smith denise0@adventure-works.com 560 Jennifer J. Maxham jennifer1@adventure-works.com 1561 Ido Ben-Sacha ido1@adventure-works.com 924 Becky R. Waters becky0@adventure-works.com
如下语句在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级的主题。
Level 2 - NonClustered.sql | Level2_MillionRowContactTable.sql