zoukankan      html  css  js  c++  java
  • 【译】索引进阶(五):带有包含列的索引

    【译注:此文为翻译,由于本人水平所限,疏漏在所难免,欢迎探讨指正】 

     原文链接:传送门

            之前的系列文章介绍了聚集索引和非聚集索引,着重讲解了以下几个方面的内容:

    •                对于表中的每一行来说,索引中总是存在这一个条目与它对应(注:此例外规则将在以后予以关注)。这些条目总是按索引键排序。
    •               在聚集索引中,索引键就是实际的表数据行。
    •               在非聚集索引中,条目是与数据行分开的,它包含一个索引键值和一个映射到实际表数据的书签值。

          此前一句话的后半部分是正确但不是完整的,在本章节我们将检查一种可选方案:给非聚集索引包含额外的列,成为“包含列(included columns)”。在第六章,我们将讲解书签操作,将会看到SQL SERVER或许会单方面的为你的索引添加一些列。

    包含列

            那些在非聚集索引中,但不是索引键的一部分的列称为包含列。这些列不属于索引的一部分,因此不会影响索引中条目的顺序。同样我们会看到,包含列比索引列导致更少的开销。

            当创建非聚集索引时,我们将包含列与索引列分开指定,如下所示:

            

    CREATE NONCLUSTERED INDEX FK_ProductID_ ModifiedDate
           ON Sales.SalesOrderDetail (ProductID, ModifiedDate)
           INCLUDE (OrderQty, UnitPrice, LineTotal)

            在这个示例中,ProductID 和 ModifiedDate 是索引键列,OrderQty,UnitPrice,LineTotal 是包含列。

            假设在上面的SQL语句中我们没有指定INCLUDE 语句,那么结果索引看起来会是这个样子:

            

    ProductID   ModifiedDate   Bookmark
    
    Page n:
    
    707         2004/07/25        =>  
    707         2004/07/26        =>  
    707         2004/07/26        =>  
    707         2004/07/26        =>  
    707         2004/07/27        =>  
    707         2004/07/27        =>  
    707         2004/07/27        =>  
    707         2004/07/28        =>  
    707         2004/07/28        =>  
    707         2004/07/28        =>  
    707         2004/07/28        =>  
    707         2004/07/28        =>  
    707         2004/07/28        =>  
    
    Page n+1:
    
    707         2004/07/29        =>  
    707         2004/07/31        =>  
    707         2004/07/31        =>  
    707         2004/07/31        =>  
    708         2001/07/01        =>  
    708         2001/07/01        =>  
    708         2001/07/01        =>  
    708         2001/07/01        =>  
    708         2001/07/01        =>  
    708         2001/07/01        =>  
    708         2001/07/01        =>  
    708         2001/07/01        =>  
    708         2001/07/01        =>  
    708         2001/07/01        =>  

             然而,既然告诉了SQL SERVER包含了OrderQty,UnitPrice 和 LineTotal 这几列,索引看起来会是这个样子:

    :- Search Key Columns -:      :---  Included Columns  ---:     : Bookmark :
    
    ProductID   ModifiedDate      OrderQty    UnitPrice   LineTotal       
    
    Page n-1:
    
    707         2004/07/29        1           34.99       34.99       =>  
    707         2004/07/31        1           34.99       34.99       =>  
    707         2004/07/31        3           34.99      104.97       =>  
    707         2004/07/31        1           34.99       34.99       =>  
    708         2001/07/01        5           20.19      100.95       =>  
    
    Page n:
    
    708         2001/07/01        1           20.19       20.19       =>  
    708         2001/07/01        1           20.19       20.19       =>  
    708         2001/07/01        2           20.19       40.38       =>  
    708         2001/07/01        1           20.19       20.19       =>  
    708         2001/07/01        2           20.19       40.38       =>  
    
    708         2001/12/01        7           20.19      141.33       =>  
    708         2001/12/01        1           20.19       20.19       =>  
    708         2002/01/01        1           20.19       20.19       =>  
    708         2002/01/01        1           20.19       20.19       =>  
    708         2002/01/01        1           20.19       20.19       =>  
    
    Page n+1:
    
    708         2002/01/01        2           20.19       40.38       =>  
    708         2002/01/01        5           20.19      100.95       =>  
    708         2002/02/01        1           20.19       20.19       =>  
    708         2002/02/01        1           20.19       20.19       =>  
    708         2002/02/01        2           20.19       40.38       =>  

            查看这个索引键显示的内容,很显然数据行是按照索引键列排序的。ProductID为708的产品,更新时间为 2002/01/01的5条数据在索引中是连续的,如同其他的ProductID/ ModifiedDate的组合行一样。

            或许你会问:“为什么要指定包含列?为什么不简单的把OrderQty,UnitPrice,LineTotal 加到索引键上?”。把这些列包含进索引中但不包含进索引键中有如下几个优点:

    •                  不是索引键一部分的列不会影响索引中条目的位置,这样便会减小把它们包含进索引的开销。举个例子,如果行中 ProductID 或者ModifiedDate 的值被更新了,那么那条数据在索引中的对应条目肯定会被重新定位,但是,如果数据行的UnitPricevalue 值被更新了,那么索引条目虽然仍然需要被更新,但是不需要移动位置。
    •                 更少的努力去定位索引条目。
    •                 索引的大小会稍微小点。
    •                 索引的数据分配统计会更容易维护。

          后续几个章节,当我们了解了索引的内部结构,以及SQL SERVER 为了优化查询性能而维护的其他额外信息后,这些优点会变得更有意义。

          决定一个索引列是索引键的一部分还是包含索引,并不是你做的最重要的索引决策。也就是说,频繁出现在SELECT 语句列表中但不在WHERE 字句的列最好就放置在包含索引列中(最佳实践)。

    成为覆盖索引

           在第四章节,我们对AdventureWorks数据库的设计者做的将SalesOrderID/ SalesOrderDetailID 作为SalesOrderDetail表的聚集索引这一决策表示认同,这张表的大部分查询都会请求按销售单号排序或者分组的数据。然而,也有许许多多的查询,或许来自于便利店的员工,需要以产品序列组织的信息。正是这些查询会从如上所示的索引中受益。

          为了演示在那个索引中带有包含索引的潜在益处,我们将 会审视SalesOrderDetail表上的两个查询,他们每一个都会执行三次,如下所示:

    •       第一次执行:无非聚集索引。
    •       第二次执行:使用没有包含列的非聚集索引(只有两个索引键)。
    •       第三次执行:使用之前定义的非聚集索引(具有包含列)。

         就像我们在之前章节所做的那样,我们再次使用逻辑读作为首要评判标准,但是我们也使用SSMS的“显示实际的执行计划”选项来查看各个执行的计划,这会给我们一个附加的评判标准:非读动作所占的工作量的百分比,比如在数据被读入内存后匹配关联的数据,这使得我们对于查询总消耗有一个更好的理解。

    测试第一个查询:商品总数

          我们的第一个查询,提供了某一个特定商品按日期分组区分的总数:

         列表5.2:

    SELECT  ProductID ,
            ModifiedDate ,
            SUM(OrderQty) AS 'No of Items' ,
            AVG(UnitPrice) 'Avg Price' ,
            SUM(LineTotal) 'Total Value'
    FROM    Sales.SalesOrderDetail
    WHERE   ProductID = 888
    GROUP BY ProductID ,
            ModifiedDate ;

             因为索引只会影响查询的性能,而不会影响查询的结果,在三种不同的索引架构下执行这个查询总会产出如下的行集:

    ProductID   ModifiedDate    No of Rows  Avg Price         Total Value
    
    ----------- ------------    ----------- -----------------------------
    888         2003-07-01      16          602.346           9637.536000
    888         2003-08-01      13          602.346           7830.498000
    888         2003-09-01      19          602.346           11444.574000
    888        2003-10-01       2           602.346           1204.692000
    888         2003-11-01      17          602.346           10239.882000
    888         2003-12-01      4           602.346           2409.384000
    888         2004-05-01      10          602.346           6023.460000
    888         2004-06-01      2           602.346           1204.692000

          输出的8行数据是从表中39行“ProductID = 888“数据聚合而来,每个日期会包含多条 ‘ProductID = 888’ 的销售记录,而我们为每个日期的多条记录生成一个输出。进行我们的测试的基本模式在列表5.3列出。在执行任何查询之前,确保执行了 SET STATISTICS IO ON 脚本。

    IF EXISTS ( SELECT  1
                FROM    sys.indexes
                WHERE   name = 'FK_ProductID_ModifiedDate'
                        AND OBJECT_ID = OBJECT_ID('Sales.SalesOrderDetail') ) 
        DROP INDEX Sales.SalesOrderDetail.FK_ProductID_ModifiedDate ;
    GO
    
    --RUN 1: Execute Listing 5.2 here (no non-clustered index)
    
    CREATE NONCLUSTERED INDEX FK_ProductID_ModifiedDate
    ON Sales.SalesOrderDetail (ProductID, ModifiedDate) ;
    
    --RUN 2: Re-execute Listing 5.2 here (non-clustered index with no include)
    
    IF EXISTS ( SELECT  1
                FROM    sys.indexes
                WHERE   name = 'FK_ProductID_ModifiedDate'
                        AND OBJECT_ID = OBJECT_ID('Sales.SalesOrderDetail') ) 
        DROP INDEX Sales.SalesOrderDetail.FK_ProductID_ModifiedDate ;
    GO
    
    CREATE NONCLUSTERED INDEX FK_ProductID_ModifiedDate
    ON Sales.SalesOrderDetail (ProductID, ModifiedDate)
    INCLUDE (OrderQty, UnitPrice, LineTotal) ;
    
    --RUN 3: Re-execute Listing 5.2 here (non-clustered index with include)
    View Code

    列表5.3:我们要执行的查询模式

         在各个索引模式下执行查询所需要的相关工作量显示在下表中:

    第一次执行:没有非聚集索引

    表SalesOrderDetail:扫描计数1次,逻辑读1238次,非读活动:8%

    第二次执行:有非聚集索引,没有包含列

    表SalesOrderDetail:扫描计数1次,逻辑读131次,非读活动:0%

    第三次执行:有非聚集索引:有包含列

    Table 'SalesOrderDetail'. Scan count 1, logical reads 3.

    Non read activity:  1%.

        从这些结果你可以看到:

        第一次执行需要对SalesOrderDetail 表的完整扫描,每一行需要被读并且检查以决定其是否参与到返回的结果集。

        第二次执行使用了非聚集索引快速的找到了所请求的39行数据的书签,但它仍然需要从表中返回各个数据行。

        第三次执行在非聚集索引中找到它需要的所有东西,并且在最优的顺序中(ModifiedDate/ProductID.),它快速跳转到第一个所请求的条目上,读取了39个连续的条目,当它读的同时对各个条目做了聚合计算,这样一切便完成了。

    测试第二个查询:基于日期的总数

           我们的第二个查询和第一个查询是相同的,只是在WHERE子句的一点小小的改变,这次仓库将请求基于日期而不是基于产品的信息。我们必须在最右边的检索键列(ModifiedDate)进行过滤,而不是最左边的列(ProductID)。新的查询展示如下:

    SELECT  ModifiedDate ,
            ProductID ,
            SUM(OrderQty) 'No of Items' ,
            AVG(UnitPrice) 'Avg Price' ,
            SUM(LineTotal) 'Total Value'
    FROM    Sales.SalesOrderDetail
    WHERE   ModifiedDate = '2003-10-01'
    GROUP BY ModifiedDate ,
            ProductID ;

           部分结果行集合展示如下:

           

    ProductID   ModifiedDate    No of Items Avg Price             Total Value
    ----------- ------------    ----------- --------------------- ----------------
                                       :
                                       :
    782         2003-10-01      62          1430.9937             86291.624000
    783         2003-10-01      72          1427.9937             100061.564000
    784         2003-10-01      52          1376.994              71603.688000
    792         2003-10-01      12          1466.01               17592.120000
    793         2003-10-01      46          1466.01               67436.460000
    794         2003-10-01      37          1466.01               54242.370000
    795         2003-10-01      22          1466.01               32252.220000
                                       :
                                       :
    (164 row(s) affected)

             WHERE条件将表数据过滤为1492 行满足条件的数据,然后它们再分组,产生了164 行数据的输出。

             在各个索引架构下执行查询所需要的工作量如下表列出所示:

            

    第一次执行:没有非聚集索引

    表SalesOrderDetail:扫描计数1次,逻辑读1238,非读活动:10%

    第二次执行:有非聚集索引,但没有包含列

    表SalesOrderDetail:扫描计数1次,逻辑读1238,非读活动10%

    第三次执行:有非聚集索引,有包含列

    表SalesOrderDetail:扫描计数1次,逻辑读761,非读活动 8%

           第一个和第二个查询导致了相同的执行计划:完整扫描了表theSalesOrderDetail 。正是因为第四章详细讲述的原因,WHERE子句没有足够的高选择性从而从非含索引中受益,除此之外,组成任何一个分组得数据行都是分散在整张表中,当表被读取时,每一行都会匹配到它的对应分组中,这些操作会消耗处理器时间和内存。第三个查询在非聚集索引中找到了它所需要的一切,但是,不像之前的查询,它发现数据行在索引中是不连续存储的,但是分组本身又延展了整个索引长度,因此,SQL SERVER扫描了整个索引。

          索引扫描比起表扫描有如下两个优点:

    •       索引比表小,因而需要更少的读取。
    •       数据已经是分组过了,需要更少的非读活动。

    结论

         对于大量的查询来说,包含列使得非聚集索引成为覆盖索引,它提升了这些查询的性能,有时颇具有戏剧性。包含列增加了索引的大小,但却几乎不增加索引的开销。任何时候当你创建非聚集索引时,尤其在一个外键列上,问下自己:“在这个索引中我应该包含什么额外的列呢?”

  • 相关阅读:
    mysql_fetch_row()获取显示数据
    数组上下移动
    croppie 在Angular8 中使用
    关于 element 的 backToTop
    苹果手机new Date()问题
    js精简代码集合
    vue 中使用高德地图, 地图选点
    代替if else 的表单验证方法!
    记一次webpack打包样式加载问题
    echarts 饼图的指示线(labelline) 问题
  • 原文地址:https://www.cnblogs.com/qianxingmu/p/10446749.html
Copyright © 2011-2022 走看看