原文地址:
Stairway to SQL Server Indexes: Level 7,Filtered Indexes
本文是SQL Server索引进阶系列(Stairway to SQL Server Indexes)的一部分。
在之前的级别中,我们已经说过,表中的每一行在索引中会生成一个入口,这条规则有一个例外。一些索引的入口会比对应的表的行数要少。这些索引被称作“过滤的索引”,是SQL Server 2008中的一个特性。
过滤一个索引
创建一个包含where子句的过滤的非聚集索引。
IF EXISTS ( SELECT * FROM sys.indexes WHERE OBJECT_ID = OBJECT_ID('Sales.SalesOrderDetail') AND name = 'FI_SpecialOfferID' ) DROP INDEX Sales.SalesOrderDetail.FI_SpecialOfferID ; GO CREATE INDEX FI_SpecialOfferID ON Sales.SalesOrderDetail (SpecialOfferID) WHERE SpecialOfferID;
过滤一个索引的主要原因,是为了从索引中消除一个或者多个不经常被选择的值。看一下SalesOrderDetail表中的SpecialOfferID列,121317行的数据包含了12个不同的SpecialOfferID值,从1到16,每个值对应的行数如下。
SpecialOfferID RowCount
-------------- -----------
1 115884
2 3428
3 606
13 524
14 244
16 169
7 137
8 98
11 84
4 80
9 61
5 2
大部分的行,超过95%,SpecialOfferID的值是1.在SpecialOfferID列的非聚集索引对于SpecialOfferID=1的查询没有好处。查询将使用表扫描来查询115884行数据。但是,所以对于SpecialOfferID=5的查询是有好处的。
本文开头的创建索引对于115884行SpecialOfferID=1的数据行没有入口。因此索引很小,很高效,只包含5433个入口。
在我们的SalesOrderDetail例子中,需要过滤的主要值是“1”。在你自己的应用中,最常见的可能是NULL。在典型的事务数据库中,在可空的列中,如果null值占多数,NOT NULL就是例外。在这些列上创建索引的时候,要考虑过滤null值。
概念证明
为了证明过滤索引的好处,我们将六次执行下面的查询:
SELECT * FROM Sales.SalesOrderDetail WHERE SpecialOfferID = parameter value ORDER BY SpecialOfferID ;
在上面的统计中可以看出来,1的行数占95%,13的行数占4%,14的行数占2%。
和往常一样,我们使用IO读取次数作为主要的衡量指标,同时打开SQL Server管理器的“显示执行计划”选项,观察每次查询的执行计划。
执行结果的统计如下。
WITH UNFILTERED INDEX: | ||
Parameter Value | Reads | Plan |
1 | 1238 | Table scan |
13 | 1238 | Table scan |
14 | 758 | Retrieve bookmark values from index. Use them to retrieve rows from table |
WITH FILTERED INDEX: | ||
Parameter Value | Reads | Plan |
1 | 1238 | Table scan |
13 | 1238 | Table scan |
14 | 758 | Retrieve bookmark values from index. Use them to retrieve rows from table. |
从上面可以看出来吗,不管用没用到索引,结果是一样的。换句话说,过滤的索引和非过滤的索引带来的好处是一样的。在不影响查询效率的情况下,我们节省了大量的磁盘空间。
过滤,查询,覆盖
在上面的例子中,对同一个索引来说,过滤列和索引键列是同一个列。当我们通过where子句指定过滤的时候,我们告诉SQL Server:“如果你查询那些SpecialOfferID<>1的行,这个索引有这些行的入口。”不管索引键是什么,让SQL Server知道这些信息都是有好处的。
想一想我们之前创建的一个索引,帮助仓库管理员查询SalesOrderDetail表中产品相关的信息。
CREATE NONCLUSTERED INDEX FK_ProductID_ModifiedDate ON Sales.SalesOrderDetail (ProductID,ModifiedDate) INCLUDE (OrderQty,UnitPrice,LineTotal) ;
上面的索引产生的结果是。
:- Search Key Columns -: : --- Included Columns ---: :--- Bookmark ---:
ProductID ModifiedDate OrderQty UnitPrice LineTotal OrderId DetailId
----------- ------------ -------- --------- --------- ----------- ----------
Page n-1:
709 01 Feb 2002 1 5.70 5.70 45329 6392
709 01 May 2002 1 5.70 5.70 46047 8601
710 01 Jul 2001 1 5.70 5.70 43670 111
710 01 Jul 2001 1 5.70 5.70 43676 152
710 01 Sep 2001 1 5.70 5.70 44075 1448
Page n:
710 01 Oct 2001 1 5.70 5.70 44303 2481
710 01 Nov 2001 1 5.70 5.70 44484 2853
710 01 Nov 2001 1 5.70 5.70 44499 3006
710 01 Nov 2001 1 5.70 5.70 44523 3346
710 01 Nov 2001 1 5.70 5.70 44527 3400
如果仓库管理员经常查询SpecialOfferID<>1的信息,很少查询=1的信息,在创建索引的时候添加一个SpecialOfferID!=1的where子句是很有意义的。结果就是很小的索引就可以覆盖大部分的请求。
CREATE NONCLUSTERED INDEX FK_ProductID_ModifiedDate ON Sales.SalesOrderDetail (ProductID,ModifiedDate) INCLUDE (OrderQty,UnitPrice,LineTotal) WHERE SpecialOfferID <>1
执行下面的查询语句。
SELECT ProductID , ModifiedDate , SUM(OrderQty) 'No of Items' , AVG(UnitPrice) 'Avg Price' , SUM(LineTotal) 'Total Value' FROM Sales.SalesOrderDetail WHERE SpecialOfferID <> 1 GROUP BY ProductID , ModifiedDate
SQL Server管理器告诉我们,过滤的索引被扫描,读取了36页,产生2102条结果。
一些警告
在决定使用过滤索引的时候,要记住两个重要的问题。
1 SQL Server如何评估过滤的索引
你可能会很奇怪,把之前查询语句的where条件从SpecialOfferID<>1变成SpecialOfferID=2,就会防止SQL Server使用过滤的索引。这是因为SQL Server比较了select查询的where子句和create index的where子句,认为他们两个语法上是相等的,而不是比较逻辑的相等。因此,SQL Server没有意识到过滤的索引覆盖了查询。
另外,你不能通过复合的where子句,例如:where SpecialOfferID<>1 and SpecialOfferID=2来促使SQL Server使用过滤的索引。在后面的级别中,我们将会给出一些提示,教给你一些影响SQL Server选择索引的能力。现在,记住SQL Server在评估过滤索引的时候,做出的是语法的决定。
2 不要使用过滤的索引来弥补不好的数据库设计
在创建过滤索引的时候,不要创建索引来弥补违反三范式的数据库设计。
大部分违反三范式的数据库设计,主要是对于实体的子类型认识错误。看一下下面的一张表。
ProductID | Description | Type | Price | Author | IssuesPerYear |
(Primary Key) | |||||
44E | Roots | Book | 44.50 | Alex Haley | |
17J | Time | Periodical | 18.00 | 52 | |
22D | Gift from the Sea | Book | 37.00 | Anne Morrow Lindbergh | |
18K | National Geographic | Periodical | 38.00 | 12 | |
78K | Good Housekeeping | Periodical | 37.00 | 12 |
很容易看出表中包含两种类型的商品:Book书和Periodical期刊。只有书才有Author作者信息,只有期刊才有IssuesPerYear每年的刊数信息。正确的方法是,一张主表来存放公共的信息,然后每个子类型附件一张表。每张表都有相同的主键,子类型表的主键也是连接主表的外键。
Products Table
ProductID | Description | Price |
(Primary Key) | ||
44E | Roots | 44.50 |
17J | Time | 18.00 |
22D | Gift from the Sea | 37.00 |
18K | National Geography | 38.00 |
78K | Good Housekeeping | 37.00 |
Books table
ProductID | Author |
(Primary Key and Foreign Key) | |
44E | Alex Haley |
22D | Anne Morrow Lindbergh |
Periodicals table
ProductID | IssuesPerYear |
(Primary Key and Foreign Key) | |
17J | 52 |
18K | 12 |
78K | 12 |
像上面的情况,可以建立过滤的索引,来过滤NULL列。
但是正确的做法是重新定义表结构。应用开发者和开发工具不知道你设计的索引,他们只能看见你的表。如果表的结构不影响业务的结构,开发者将会尽力的构建和维护数据库之上的应用。
结论
过滤的索引消除了索引中无用的入口,产生的索引更小,更有利于查询。过滤的索引是通过在create index中指定where子句来实现的。在where子句中的列不同于索引键的列,也不同于include子句中的列。
如果一张表中的一个子集经常被访问,过滤的索引也能是一个覆盖的索引,也可以导致IO有一个相当大的减少。
不要将创建过滤索引作为正确设计数据库的替代选择。