原文连接:http://www.sqlservercentral.com/articles/Stairway+Series/72276/sql
包含列的索引:通往SQL Server索引级别5的阶梯数据库
大卫•杜兰特2011/07/13数据库设计
该系列sqlserver
本文是楼梯系列的一部分:SQL Server索引的阶梯性能
索引是数据库设计的基础,并告诉开发人员使用数据库很是了解设计器的意图。不幸的是,当性能问题出现时,索引经常被添加到过后。这里最后是一个简单的系列文章,它应该能让任何数据库专业人员快速“跟上”他们的步伐测试
前面的级别引入了集群和非汇集索引,突出了每一个方面的如下方面:优化
表中的每一行都有一个条目(咱们注意到这个规则的例外状况将在之后的级别中被覆盖)。这些条目老是在索引键序列中。设计
在汇集索引中,索引项是表的实际行。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书签
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列,索引看起来是这样的:
--- --- --- --- --- --- --- --- --- --- --- --
产品修改日期
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(以粗体显示)的5行,在索引中是连续的,就像其余全部ProductID / ModifiedDate组合中的行同样。
你可能会问“为何要包含列呢?”为何不直接向索引键添加OrderQty、UnitPrice和LineTotal ?“在索引中有这些列有几个优势,但索引键没有,好比:
不属于索引键的列不会影响索引内条目的位置。这反过来下降了在索引中使用它们的开销。例如,若是行中的ProductID或ModifiedDate值被修改,那么该行的条目必须在索引中从新定位。可是,若是在行中的unit订价evalue被修改,那么索引项仍然须要更新,但它不须要移动。
在索引中定位一个条目所需的工做量更少。
指数的大小将会稍微小一些。
索引的数据分布统计数据将更容易维护。
当咱们查看索引的内部结构以及SQL Server维护的一些额外信息以优化查询性能时,这些优点在之后的级别中会更有意义。
决定一个索引列是不是索引键的一部分,或者仅仅是一个包含的列,并非您所要作的最重要的索引决定。也就是说,在SELECT列表中常常出现的列,而不是查询的WHERE子句中最优的列在索引的列中。
成为一种覆盖指数
在第4级,咱们与AdventureWorksdatabase的设计人员达成协议,他们决定让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修改日期不为全部行Avg价格总值
----------- ------------ ----------- -----------------------------
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
——运行1:在这里执行清单5.2(没有非汇集索引)
CREATE NONCLUSTERED INDEX FK_ProductID_ModifiedDate
ON Sales.SalesOrderDetail (ProductID, ModifiedDate) ;
——运行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) ;
——运行3:在这里从新执行清单5.2(包含包含的非汇集索引)
清单5.3:测试“产品的活动总数”查询
对每一个索引方案执行查询所需的相对工做如表5.1所示。
1:运行
没有非汇集索引
表“SalesOrderDetail”。扫描计数1,逻辑读1238。
非阅读活动:8%。
运行2:
索引-不包括列
表“SalesOrderDetail”。扫描计数1,逻辑读131。
非阅读活动:0%。
运行3:
包括列
表“SalesOrderDetail”。扫描计数1,逻辑读3。
非阅读活动:1%。
表5.1:使用不一样的非汇集索引运行第一个查询的结果三次
从这些结果能够看出:
运行1须要对SalesOrderDetail表进行完整的扫描;每一行都必须阅读和检查,以肯定是否应该参与结果。
Run 2使用非汇集索引快速查找39个请求行的书签,但它必须从表中逐个检索这些行。
运行3在非汇集索引中找到所需的全部内容,并在ProductID内最有利的序列中进行修改。它迅速跳到第一个请求的条目,读了39个连续的条目,在读取的每一个条目上作汇总计算,而后完成了。
测试第二个查询:基于日期的活动总数
咱们的第二个查询与第一个查询彻底相同,只是在WHERE子句中发生了更改。这一次,仓库是根据日期请求信息,而不是基于产品。咱们必须在最右的搜索键栏上进行过滤,修改日期;而不是最左边的列,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:“按日期执行的活动总数”查询
产生的行集,部分是:
产品的修改日期不包括价格总额
----------- ------------ ----------- --------------------- ----------------
:
:
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所示。
1:运行
没有非汇集索引
表“SalesOrderDetail”。扫描计数1,逻辑读1238。
非阅读活动:10%。
运行2:
索引-不包括列
表“SalesOrderDetail”。扫描计数1,逻辑读1238。
非阅读活动:10%。
运行3:
包括列
表“SalesOrderDetail”。扫描计数1,逻辑读761。
非阅读活动:8%。
表2:使用不一样的非汇集索引运行第二个查询的结果
第一次和第二次测试都产生了相同的计划;一个完整的扫描详细信息表。因为第4级中详细讨论的缘由,WHERE子句没有足够的选择性从非覆盖索引中获益。并且,包含任何一个组的行分布在整个表中。在读取表时,每一行必须与组相匹配;以及消耗处理器时间和内存的操做。
第三个测试在非汇集索引中找到了它所须要的一切;可是,与前面的查询不一样,它没有发现索引中相邻的行。在索引中,包含每一个组的行是连续的;但这些组织自己分散在指数的长度上。所以,SQL Server扫描索引。
扫描索引而不是表格有两个优势:
该指数小于表,要求更少的读数。
这些行已经分组,须要更少的非读活动。
结论
包含的列使非汇集索引可以成为各类查询的索引,从而提升这些查询的性能;有时会很显著。包含的列增长了索引的大小,但在开销方面却没有增长。任什么时候候建立非汇集索引,尤为是在外键列上,都要问本身:“在这个索引中应该包含哪些额外的列?”
本文是通往SQL Server索引楼梯的楼梯的一部分