包含列的索引:SQL Server索引的阶梯级别5

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了包含列的索引:SQL Server索引的阶梯级别5相关的知识,希望对你有一定的参考价值。

原文链接:http://www.sqlservercentral.com/articles/Stairway+Series/72276/ 作者David Durant,2011/07/13 该系列 本文是“Stairway系列:SQL Server索引的阶梯”的一部分 索引是数据库设计的基础,并告诉开发人员使用数据库关于设计者的意图。不幸的是,当性能问题出现时,索引往往被添加为事后考虑。这里最后是一个简单的系列文章,应该使他们快速地使任何数据库专业人员“快速” 前面的级别引入了聚簇和非聚簇索引,突出了以下各方面: ?表中每一行的索引总是有一个条目(我们注意到,这个规则的一个例外将在后面的级别中进行讨论)。这些条目始终处于索引键序列中。 ?在聚簇索引中,索引条目是表的实际行。 ?在非聚集索引中,条目与数据行分开;由索引键列和书签值组成,以将索引键列映射到表的实际行。 前面句子的后半部分是正确的,但不完整。在这个级别中,我们检查选项以将其他列添加到非聚集索引(称为包含列)。在检查书签操作的级别6中,我们将看到SQL Server可能会单方面向您的索引添加一些列。 包括列 在非聚集索引中但不属于索引键的列称为包含列。这些列不是键的一部分,因此不影响索引中条目的顺序。而且,正如我们将会看到的那样,它们比键列造成的开销更少。 创建非聚集索引时,我们指定了与键列分开的包含列;如清单5.1所示。

CREATE NONCLUSTERED INDEX FK_ProductID_ ModifiedDate

       ON Sales.SalesOrderDetail (ProductID, ModifiedDate)

       INCLUDE (OrderQty, UnitPrice, LineTotal)

清单5.1:创建包含列的非聚集索引 在本例中,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       =>  

检查显示的这个索引的内容,显然这些行按索引键列排序。例如,修改日期为2002年1月1日(以粗体突出显示)的产品708的五行在索引中是连续的,每隔一个ProductID / ModifiedDate组合的行也是如此。 你可能会问“为什么甚至包括列?为什么不简单地将OrderQty,UnitPrice和LineTotal添加到索引键?“索引中有这些列但索引键中没有这些列有几个优点,例如: ?不属于索引键的列不会影响索引内条目的位置。这反过来又减少了让他们在索引中的开销。例如,如果行中的ProductID或ModifiedDate值被修改,那么该行的条目必须在索引内重新定位。但是,如果行中的UnitPricevalue被修改,索引条目仍然需要更新,但不需要移动。 ?在索引中查找条目所需的努力较少。 ?索引的大小会略小。 ?索引的数据分布统计将更容易维护。 当我们查看索引的内部结构以及由SQL Server维护的用于优化查询性能的一些附加信息时,大多数这些优势在以后的级别中将更有意义。 确定索引列是否是索引键的一部分,或只是包含的列,不是您将要做的最重要的索引决定。也就是说,频繁出现在SELECT列表中但不在查询的WHERE子句中的列最好放在索引的包含列部分。 成为覆盖指标 在级别4中,我们表示与AdventureWorks数据库的设计者达成协议,决定将SalesOrderID / SalesOrderDetailID作为SalesOrderDetail表的聚集索引。针对此表的大多数查询都将请求按销售订单编号排序或分组的数据。然而,可能来自仓库人员的一些查询将需要产品序列中的信息。这些查询将受益于清单5.1所示的索引。 为了说明在索引中包含列的潜在好处,我们将查看两个针对SalesOrderDetailtable的查询,每个查询我们将执行三次,如下所示: ?运行1:没有非聚集索引 ?运行2:使用不包含列的非聚簇索引(只有两个关键列) ?运行3:使用清单5.1中定义的非聚集索引 正如我们在前面的级别所做的那样,我们再次使用读取次数作为主要度量标准,但是我们也使用SQL Server Management Studio的“显示实际执行计划”选项来查看每个执行的计划。这会给我们一个额外的指标:在非读取活动上花费的工作量的百分比,例如在将相关数据读入内存之后进行匹配。这使我们更好地了解查询的总成本。 测试第一个查询:产品的活动总数 清单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 ;

清单5.2:“按产品的活动总计”查询 由于索引可以影响查询的性能,但不影响结果; 对这三个不同的索引方案执行这个查询总是产生下面的行集合:

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

这八行输出从表中的三十九个“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:在这里执行清单5.2(没有非聚集索引)

CREATE NONCLUSTERED INDEX FK_ProductID_ModifiedDate

ON Sales.SalesOrderDetail (ProductID, ModifiedDate) ;

 

--RUN 2:在这里重新执行清单5.2(不包含非聚集索引)

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:在这里重新执行清单5.2(包含非聚簇索引)

清单5.3:测试“按产品的活动总计”查询 5.1显示了对每个索引方案执行查询所需的相对工作量。

Run 1:

No Nonclustered Index

Table ‘SalesOrderDetail‘. Scan count 1, logical reads 1238.

Non read activity:  8%.

Run 2:

Index – No Included Columns

Table ‘SalesOrderDetail‘. Scan count 1, logical reads 131.

Non read activity:  0%.

Run 3:

With Included Columns

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

Non read activity:  1%.

5.1:使用不同的非聚集索引可运行第一次查询三次的结果 正如你可以从这些结果看到的: ?运行1需要完整扫描SalesOrderDetail表;每一行都必须阅读和检查,以确定是否应该参与结果。 ?运行2使用非聚簇索引为39个请求的行快速查找书签,但必须从表中单独检索每个行。 ?运行3在非聚集索引中找到所需的所有内容,并以最有利的顺序 - 产品ID中的ModifiedDate。它迅速跳到第一个要求的条目,阅读了39个连续的条目,对每个条目进行了总计算,读取完成。 测试第二个查询:基于日期的活动总数 我们的第二个查询与第一个查询是相同的,除了WHERE子句的更改。这次仓库正在根据日期而不是产品请求信息。我们必须过滤最右边的搜索键列ModifiedDate;而不是最左边的一列ProductID。新的查询如清单5.4所示。

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 ;

清单5.4:“按日期的活动总计”查询 生成的行集部分是:

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行的输出。 要运行测试,请按照代码5.3中所述的相同方案,但使用代码清单5.4中的新查询。 结果是表5.2显示了对每个索引方案执行查询所需的相对工作量。

Run 1:

No Nonclustered Index

Table ‘SalesOrderDetail‘. Scan count 1, logical reads 1238.

Non read activity:  10%.

Run 2:

With Index – No Included Columns

Table ‘SalesOrderDetail‘. Scan count 1, logical reads 1238.

Non read activity:  10%.

Run 3:

With Included Columns

Table ‘SalesOrderDetail‘. Scan count 1, logical reads 761.

Non read activity:  8%.

2:使用可用的不同非聚簇索引三次运行第二个查询的结果 第一次和第二次测试都是相同的计划。对SaleOrderDetail表的完整扫描。由于第4级中详细说明的原因,WHERE子句没有足够的选择性从非覆盖索引中受益。而且,包含任何一个组的行都散布在整个表格中。正在读表时,每一行都必须与其组相匹配。以及消耗处理器时间和内存的操作。 第三个测试发现了它在非聚集索引中需要的一切;但与前面的查询不同,它没有找到索引内连续的行。构成每个单独组的行在索引内是连续的;但是这些群体本身分散在指数的长度上。因此,SQL Server扫描索引。 扫描索引而不是表格有两个好处: ?索引小于表,需要更少的读取。 ?行已经分组,需要较少的非阅读活动。 结论 包含的列使非聚集索引能够覆盖各种查询的索引,从而提高这些查询的性能;有时相当戏剧性。包含的列增加了索引的大小,但在开销方面增加了很少的内容。任何时候你创建一个非聚集索引,特别是在一个外键列上,问自己 - “我应该在这个索引中包含哪些额外的列?”

以上是关于包含列的索引:SQL Server索引的阶梯级别5的主要内容,如果未能解决你的问题,请参考以下文章

包含列的索引:通往SQL Server索引级别5的阶梯

SQL Server索引内部结构:SQL Server索引的阶梯级别10

SQL Server索引内部结构:SQL Server索引的阶梯级别10

5级阶梯SQL Server索引

聚簇索引:SQL Server索引级别3

阅读查询计划:SQL Server索引级别9