索引进阶第一节总体上介绍了SQL SERVER索引的概念,着重介绍了非聚集索引。

      正如我们在第一个例子所研究的那样,我们演示了在仅仅返回一行数据时,索引的潜在的益处。在本章,我们将继续调查非聚集索引,检查它在不限于返回一行数据时(返回多条数据时),对于提高查询性能的贡献。

     和我们在本系列的其他章节的情况类似,我们首先介绍一些基础理论,检查一些索引底层细节来帮助我们解释这些理论,然后再执行一些查询。这些查询在索引存在和不存在的情况下被分别执行,由于我们开启了性能报告统计功能,通过对比,我们可以看到索引的存在对这些查询的影响。

Contact 表。我们将只使用一个索引,就是我们在第一章使用的FullName 索引来演示我们的观点。为了保证能够控制Contact 表的索引,我们在dbo架构下创建Contact 表的两份拷贝,并只在其中的一个拷贝上创建FullName 索引,这将提供给我们一个可控的环境,两个完全相同的备份表,其中一个只有一个非聚集索引,而另一个没有任何索引。

     列表1的脚本创建了Person.Contact 表的备份,任何时候我们想以一个全新的状态开始,便可以执行此脚本:



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 表的拷贝



 Contacts表的片段如下所示:



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
.



非聚集索引条目

FullName



CREATE INDEX FullName
           ON Contacts_index
    ( LastName, FirstName );



      记住一点就是,非聚集索引按顺序存储索引键,并保存着一份书签用来访问实际的表数据,你可以认为书签是某种类型的指针。接下来的章节我们会更详细描述书签的形式和用处。

     如下所示是FullName索引的片段,包含 LastName,FirstName组成的索引键, 以及一个书签:



:---      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                   =>



每一个条目包含了索引键列(LastName,FirstName)以及书签值。除此之外,一个SQL SERVER非聚集索引条目还有一些仅供内部使用的头信息,或许还会包含一些可选的数据值,这些内容后续章节会介绍,此刻对于初步理解非聚集索引来说,这些内容也是无关紧要的。

      此刻,我们需要知道的是键值使得SQL SERVER能够找到合适的索引条目,并且索引条目的书签值使得SQL SERVER 能够与实际的数据行进行交互。

序列化索引条目的益处

      索引条目按索引键排列,因而SQL SERVER可在任一方向上快速的横贯整个索引条目,对于序列化条目的扫描可以从索引的起始位置开始,也可以从索引的末尾开始,其实,从索引内单位任何一个条目开始都是可行的。

       因此,如果一个请求需要返回所有的 LastName以字母“S”开头contacts 数据,SQL SERVER可以快速导航至第一个“S”条目,然后横贯整个索引,用书签数据来访问数据行,直到它到达第一个“T”条目,此刻它便知道已经返回了所有的“S”条目。

       如果所有请求的数据行都存在于索引中,那么上述请求会执行得更快,因此,如果我们有如下查询:



SELECT FirstName, LastName 
    FROM Contact 
    WHERE LastName LIKE 'S%';



SQL SERVER可以快速导航至第一个“S”条目,然后横贯索引条目,忽略书签数据,然后直接返回索引条目,直到它到达第一个“T”条目,用关系数据库术语来说,这个索引涵盖了这个查询。

        能从序列化数据受益的任何SQL操作符都能从索引受益,这些操作符包括:ORDER BY, GROUP BY, DISTINCT, UNION (not UNION ALL), 以及JOIN…ON。

        举个例子,如果一个查询请求按照last name查询 contacts 的数量,SQL SERVER能够在第一个条目处开始计数,然后顺着索引进一步处理,每次last name的值发生变化,SQL SERVER输出当前数量并开始一个新的计数,像前面的查询一样,这也是一个涵盖的查询(Covered query):SQL SERVER仅仅访问索引,而完全忽略了表数据。

从左至右顺序的重要性,对于一个查询请求,如果查询Last Name 是 “Ashton”,那么我们的索引是很有帮助,但对于一个查询,如果它请求First Name是“Ashton”, 那么我们的索引几乎没有任何帮助。

测试一些示例查询

       如果你想执行如下的测试查询,确保你已经执行了脚本,创建了两个版本的Contact 表:dbo.Contacts_index and dbo.Contacts_noindex,并且在dbo.Contacts_index.表上已经创建了FullName索引。

       为了验证前面部分的断言,我们开启在第一节使用过的性能统计功能,并且在索引存在和不存在的情况下执行一些查询。

       



SET STATISTICS io ON
SET STATISTICS time ON



      因为AdventureWorks 的Contacts 表仅仅有19972 行数据,我们很难获取到有意义的统计时间值,我们的大部分查询都会展示一个为0的CPU时间,所以我们不从统计时间来展示输出,而仅仅从统计IO来展示输出,其反应了需要被读入的页的可能数量,这些值允许我们有一个直观的感觉来对比查询,来决定哪些查询用哪些索引会表现性能优异。如果你需要一张大表来进行更现实的时间测试,一个用于创建百万级别树Contact 表的脚本在本文是可用的。接下来的所有讨论假设你用的是标准的19972行的Contacts 表。

  测试一个涵盖查询(Testing a Covered Query)

         我们的第一个查询是一个被索引包含的查询,它返回了last name以“S”开头的所有contacts 数据的有限列集合。表 2.1给出了查询执行信息。       

 

SQL

SELECT FirstName, LastName
FROM dbo.Contacts  -- execute with both Contacts_noindex and
-- Contacts_index
WHERE LastName LIKE 'S%'

无索引

(2130行被影响)

Contacts_noindex表扫描1次,逻辑读568次

有索引

(2130行被影响)

Contacts_index 表扫描1次,逻辑读14次

索引影响

IO 从568次读减少到14次读

结论

索引包含查询是有益的事情,如果不存在索引,进行整表扫描来查找需要的数据行。

“2130行”显示出对于last name来说,“S”是一个流行的初始字母,在整个表中占据差不多10%

     测试一个非涵盖查询(Testing a Non-Covered Query)

     接下来我们更改我们的查询,和之前返回一样的行,但是包含了不在索引中的列,执行信息如下:      

 

SQL

SELECT *
FROM dbo.Contacts  -- execute with both Contacts_noindex and
-- Contacts_index
WHERE LastName LIKE 'S%'

无索引

(2130行被影响)

Contacts_noindex表扫描1次,逻辑读568次

有索引

(2130行被影响)

Contacts_index 表扫描1次,逻辑读568次

索引影响

几乎无任何影响

结论

查询执行过程中没有用过索引。

SQL SERVER决定从索引条目跳转到所需要的数据2310次会做更多的工作,于是决定扫描整个表来找到它所需要的2310行数据。

 测试一个更选择性的非涵盖查询(Testing a Non-Covered Query but Being More Selective)

         这次我们使查询更具有选择性,也就是说,我们变窄了请求的行数,这增加了索引对于查询的合适程度,查询执行信息如下:

       

 

SQL

SELECT *
FROM dbo.Contacts  -- execute with both Contacts_noindex and
-- Contacts_index
WHERE LastName LIKE 'Ste%'

无索引

和之前一样,因为是使用表扫描

有索引

(107行被影响)

Contacts_index 表扫描1次,逻辑读111次

索引影响

IO从568减少至111次

结论

SQL SERVER访问107个条目,它们按顺序在索引中,每一个条目的书签被使用来返回其交互行数据,这些行数据在表中无顺序排列。

索引对于此查询是有益的,但不如它对于第一个查询那么有益,尤其对于IO数减少的程度来说。

也许你期望读107次索引加上读107次数据,会需要107+107次读操作,然而其实只有111次读,原因后续章节会说明。此刻我们只告诉你,只有很少的几次读用来获取索引数据,大部分读操作是用来获取行数据。

SQL SERVER在背后做了大量的计算来决定是否使用索引。

测试一个涵盖聚合查询(Testing a Covered Aggregate Query)

       我们的上一个示例查询其实是一个聚合查询,那是涉及求和,计数,求平均值的查询,在这个例子中,这个查询告诉了我们在Contact 表中名字出现重复的一种趋势。部分结果看起来像这样:



Steel         Merrill             1
Steele        Joan                1
Steele        Laura               2
Steelman      Shanay              1
Steen         Heidi               2
Stefani       Stefano             1
Steiner       Alan                1



      如下是此查询的执行信息:

 

SQL

SELECT LastName, FirstName, COUNT(*) as 'Contacts'
FROM dbo.Contacts  -- execute with both Contacts_noindex and
-- Contacts_index
WHERE LastName LIKE 'Ste%'
GROUP BY LastName, FirstName

无索引

表扫描,扫描1次,568次读

存在索引

(104行被影响)表Contacts_index:扫描计数1次,逻辑读4次

索引影响

IO:从568次读减少至4次读。

结论

此查询需要的所有信息都在索引中,所有符合条件的条目均在索引中连续,不需要访问数据表,不需要排序中间结果,再次我们证明了涵盖了查询的索引是一件好事情。

测试一个非涵盖的聚合查询(Testing a Non-Covered Aggregate Query)

      如果我们更改查询使用它包含不在索引中的列,我们将得到如下的性能结果: 

 

SQL

SELECT LastName, FirstName,MiddleName, COUNT(*) as 'Contacts'
FROM dbo.Contacts  -- execute with both Contacts_noindex and
-- Contacts_index
WHERE LastName LIKE 'Ste%'
GROUP BY LastName, FirstName

无索引

表扫描,扫描1次,568次读

存在索引

(105行被影响)表ContactLarge:扫描计数1次,逻辑读111次

索引影响

IO:从568次逻辑读减少至111次逻辑读。与之前的非涵盖查询类似。

结论

处理查询所完成的中间工作并不总是出现在统计中,使用内存和tempdb对数据进行排序和合并的技术正是这样的例子,在实际中,索引的益处可能比统计列出的更大。

结论

      我们现在知道了非聚集索引具有如下特性,一个非聚集索引:

  •             是一组有序的条目
  •             每一个数据行都有一个条目
  •             包含索引键和书签
  •             你创建的
  •             SQL SERVER维护的
  •             SQL SERVER使用它来最小化努力以满足客户端请求

        我们已经见到了一些示例,有些SQL SERVER仅仅依靠索引变能满足请求,有些查询SQL SERVER则完全忽略了索引,还有一些查询使用了索引和表的组合,正因为如此,我们通过更新在第一章开头所做的表述来关闭第二章的说明。

        当一个请求到达你的数据库时候,SQL SERVER仅仅具有三种可能的方式来访问所请求的数据:

  •           仅仅访问非聚集索引而避免访问数据表,如果索引包含所有的数据,这才是有可能的。
  •           使用索引键访问非聚集索引,然后使用选择好的书签来访问各个数据行。
  •           忽略非聚集索引,进行表扫描以获取到所请求的数据。

        通常第一个理想的,第二个比第三个更可取,在后续章节,我们将展示你可以做什么来增加可行性,使得你的索引包含流行的查询,以及如何你能确定是否你的非包含查询足够具有选择性而能够从索引中受益。但那需要关于索引内部架构更详细的信息,此刻列出的知识还远远不够。

        在我们能够到达那个点之前,我们需要介绍另外一种类型的SQL SERVER 索引,聚集索引,而这正是第三章的主题。