筛选索引设计准则

筛选索引是一种经过优化的非聚集索引,尤其适用于涵盖从定义完善的数据子集中选择数据的查询。筛选索引使用筛选谓词对表中的部分行进行索引。与全表索引相比,设计良好的筛选索引可以提高查询性能、减少索引维护开销并可降低索引存储开销。

筛选索引与全表索引相比具有以下优点:

  • 提高了查询性能和计划质量

    设计良好的筛选索引可以提高查询性能和执行计划质量,因为它比全表非聚集索引小并且具有经过筛选的统计信息。与全表统计信息相比,经过筛选的统计信息更加准确,因为它们只涵盖筛选索引中的行。

  • 减少了索引维护开销

    仅在数据操作语言 (DML) 语句对索引中的数据产生影响时,才对索引进行维护。与全表非聚集索引相比,筛选索引减少了索引维护开销,因为它更小并且仅在对索引中的数据产生影响时才进行维护。筛选索引的数量可以非常多,特别是在其中包含很少受影响的数据时。同样,如果筛选索引只包含频繁受影响的数据,则索引大小较小时可以减少更新统计信息的开销。

  • 减少了索引存储开销

    在没必要创建全表索引时,创建筛选索引可以减少非聚集索引的磁盘存储开销。可以使用多个筛选索引替换一个全表非聚集索引而不会明显增加存储需要。

设计注意事项

为了设计有效的筛选索引,必须了解应用程序使用哪些查询以及这些查询与您的数据子集有何关联。例如,所含值中大部分为 NULL 的列、含异类类别的值的列以及含不同范围的值的列都属于具有定义完善的子集的数据。以下设计注意事项提供了筛选索引优于全表索引的各种情况。

数据子集的筛选索引

在列中只有少量相关值需要查询时,可以针对值的子集创建筛选索引。例如,当列中的值大部分为 NULL 并且查询只从非 NULL 值中进行选择时,可以为非 NULL 数据行创建筛选索引。由此得到的索引与对相同键列定义的全表非聚集索引相比,前者更小且维护开销更低。

例如,AdventureWorks 数据库中有一个包含 2679 行的 Production.BillOfMaterials 表。EndDate 列只有 199 行包含非 NULL 值,其余 2480 行均包含 NULL。下面的筛选索引将涵盖这样的查询:返回在此索引中定义的列的查询,以及只选择 EndDate 值不为 NULL 的行的查询。

USE AdventureWorks;
GO
IF EXISTS (SELECT name FROM sys.indexes
    WHERE name = N'FIBillOfMaterialsWithEndDate'
    AND object_id = OBJECT_ID (N'Production.BillOfMaterials'))
DROP INDEX FIBillOfMaterialsWithEndDate
    ON Production.BillOfMaterials
GO
CREATE NONCLUSTERED INDEX FIBillOfMaterialsWithEndDate
    ON Production.BillOfMaterials (ComponentID, StartDate)
    WHERE EndDate IS NOT NULL ;
GO

筛选索引 FIBillOfMaterialsWithEndDate 对下面的查询有效。您可以显示查询执行计划,以确定查询优化器是否使用了此筛选索引。有关如何显示查询执行计划的信息,请参阅分析查询

SELECT ProductAssemblyID, ComponentID, StartDate 
FROM Production.BillOfMaterials
WHERE EndDate IS NOT NULL 
    AND ComponentID = 5 
    AND StartDate > '01/01/2008' ;
GO

有关如何创建筛选索引以及如何定义筛选索引谓词表达式的详细信息,请参阅 CREATE INDEX (Transact-SQL)

异类数据的筛选索引

表中含有异类数据行时,可以为一种或多种类别的数据创建筛选索引。

例如,Production.Product 表中列出的每种产品均分配到一个 ProductSubcategoryID,后者又与 Bikes、Components、Clothing 或 Accessories 产品类别关联。这些类别为异类类别,因为它们在 Production.Product 表中的列值并不是紧密相关的。例如,对于每种产品类别,Color、ReorderPoint、ListPrice、Weight、Class 和 Style 均具有唯一特征。假设会经常查询具有子类别 27-36 的 Accessories。通过对 Accessories 子类别创建筛选索引,可以提高对 Accessories 的查询的性能。

下面的示例对 Production.Product 表中 Accessories 子类别中的所有产品创建一个筛选索引。

USE AdventureWorks;
GO
IF EXISTS (SELECT name FROM sys.indexes
    WHERE name = N'FIProductAccessories'
    AND object_id = OBJECT_ID ('Production.Product'))
DROP INDEX FIProductAccessories
    ON Production.Product;
GO
CREATE NONCLUSTERED INDEX FIProductAccessories
    ON Production.Product (ProductSubcategoryID, ListPrice) 
        Include (Name)
WHERE ProductSubcategoryID >= 27 AND ProductSubcategoryID <= 36;
GO

筛选索引 FIProductAccessories 涵盖下面的查询,因为查询结果包含在该索引中,并且查询计划不包括基表查找。例如,查询谓词表达式 ProductSubcategoryID = 33 是筛选索引谓词 ProductSubcategoryID >= 27 和 ProductSubcategoryID <= 36 的子集,查询谓词中的 ProductSubcategoryID 和 ListPrice 列全都是索引中的键列,并且名称作为包含列存储在索引的叶级别。

SELECT Name, ProductSubcategoryID, ListPrice
FROM Production.Product
WHERE ProductSubcategoryID = 33 AND ListPrice > 25.00 ;
GO

视图与筛选索引

视图是存储查询定义的虚拟表;与筛选索引相比,其用途更广,功能更强。有关视图的详细信息,请参阅了解视图使用视图的情况。下表比较了在视图和筛选索引中可以使用的部分功能。

在表达式中允许

视图

筛选的索引

计算列

联接

多个表

谓词中的简单比较逻辑*

谓词中的复杂逻辑**

*有关谓词中的简单比较逻辑,请参阅 CREATE INDEX 中的 WHERE 子句语法。

**有关谓词中的复杂比较逻辑,请参阅 SELECT 中的 WHERE 子句语法。

不能对视图创建筛选索引。但是,查询优化器可以从对视图中引用的表定义的筛选索引中获益。对于从视图中选择数据的查询,如果查询结果正确,查询优化器会考虑对此查询使用筛选索引。下面的示例创建一个视图(开始日期在 2000 年 4 月 1 日以后)和一个筛选索引(开始日期在 2000 年 8 月 1 日以后)。

USE AdventureWorks;
GO
IF OBJECT_ID ('ViewOnBillOfMaterials') IS NOT NULL
DROP VIEW ViewOnBillOfMaterials;
GO
CREATE VIEW ViewOnBillOfMaterials AS 
SELECT ComponentID, StartDate, EndDate, StartDate + 2 AS ShipDate
FROM Production.BillOfMaterials
WHERE StartDate > '20000401';
GO
IF EXISTS (SELECT name FROM sys.indexes
    WHERE name = N'FIBillOfMaterialsByStartDate'
    AND object_ID = OBJECT_ID (N'Production.BillOfMaterials'))
DROP INDEX FIBillOfMaterialsByStartDate 
    ON Production.BillOfMaterials;
GO
CREATE NONCLUSTERED INDEX FIBillOfMaterialsByStartDate
    ON Production.BillOfMaterials (ComponentID, StartDate, EndDate)
WHERE StartDate > '20000801';
GO

在下面的示例中,查询选择 2000 年 9 月 1 日以后的开始日期,这些日期完全包含在此筛选索引和筛选视图中。查询优化器将考虑使用筛选索引 FIBillOfMaterialsByStartDate,因为其中包含了正确的查询结果。

SELECT StartDate, ComponentID FROM ViewOnBillOfMaterials
WHERE StartDate > '20000901';
GO

在下一示例中,查询选择 2000 年 6 月 1 日以后的开始日期,这些日期完全包含在此视图中,但未完全包含在此筛选索引中。查询优化器不考虑使用筛选索引 FIBillOfMaterialsByStartDate,因为与查询从视图中选择数据所返回的正确结果相比,查询使用筛选索引会返回不同的结果。

SELECT StartDate, ComponentID FROM ViewOnBillOfMaterials
WHERE StartDate > '20000601';
GO

索引视图与筛选的索引的对比

筛选的索引与索引视图相比,具有以下优点:

  • 减少了索引维护开销。例如,相对于索引视图而言,查询处理器使用更少的 CPU 资源便可更新筛选的索引。

  • 改善了计划质量。例如,在查询编译期间,查询优化器考虑使用筛选的索引的情况要比考虑使用等效的索引视图的情况多。

  • 联机索引重新生成。您可以在筛选的索引可用于查询时重新生成它们。索引视图不支持联机索引重新生成。有关详细信息,请参阅 ALTER INDEX (Transact-SQL) 的 REBUILD 选项。

  • 非唯一索引。筛选的索引可以是非唯一的,而索引视图必须是唯一的。

出于以上原因,建议尽可能使用筛选的索引,不使用索引视图。如果满足以下条件,则可以使用筛选的索引而不使用索引视图:视图只引用一个表,查询不返回计算列且视图谓词使用简单的比较逻辑。例如,允许在视图定义中使用如下谓词表达式,但不允许在筛选索引中使用它,因为它包含 LIKE 运算符。

WHERE StartDate > '20000701' AND ModifiedDate LIKE 'E%'

键列

最好在筛选索引定义中包含少量的键或包含列,并且只包含查询优化器为查询执行计划选择筛选索引所需的列。无论某一筛选索引是否涵盖了查询,查询优化器都可以为查询选择此筛选索引。但是,如果某一筛选索引涵盖了查询,则查询优化器更有可能选择此筛选索引。有关涵盖查询的详细信息,请参阅创建带有包含列的索引

在某些情况下,筛选索引涵盖查询,但没有将筛选索引表达式中的列作为键或包含列包括在筛选索引定义中。以下准则说明了筛选索引表达式中的列何时应为筛选索引定义中的键或包含列。这些示例引用了此前创建的筛选索引 FIBillOfMaterialsWithEndDate。

如果筛选索引表达式等效于查询谓词并且查询并未在查询结果中返回筛选索引表达式中的列,则筛选索引表达式中的列不需要作为筛选索引定义中的键或包含列。例如,FIBillOfMaterialsWithEndDate 涵盖下面的查询,因为查询谓词等效于筛选表达式,并且查询结果中未返回 EndDate。FIBillOfMaterialsWithEndDate 不需要将 EndDate 作为筛选索引定义中的键或包含列。

SELECT ComponentID, StartDate FROM Production.BillOfMaterials
WHERE EndDate IS NOT NULL;
GO

如果查询谓词在不与筛选索引表达式等效的比较中使用了筛选索引表达式中的某列,则该列应为筛选索引定义中的键或包含列。例如,FIBillOfMaterialsWithEndDate 对下面的查询有效,因为它从筛选索引中选择了行的子集。但是,它不涵盖下面的查询,因为在比较 EndDate > '20000101' 中使用了 EndDate,此比较不与筛选索引表达式等效。查询处理器在不查找 EndDate 值的情况下无法执行此查询。因此,EndDate 应为筛选索引定义中的键或包含列。

SELECT ComponentID, StartDate FROM Production.BillOfMaterials
WHERE EndDate > '20000101';
GO

如果筛选索引表达式中的某列在查询结果集中,则该列应为筛选索引定义中的键或包含列。例如,FIBillOfMaterialsWithEndDate 不涵盖下面的查询,因为它在查询结果中返回了 EndDate 列。因此,EndDate 应为筛选索引定义中的键或包含列。

SELECT ComponentID, StartDate, EndDate FROM Production.BillOfMaterials
WHERE EndDate IS NOT NULL;
GO

表的主键不需要是筛选索引定义中的键或包含列。主键自动包含在所有非聚集索引(包括筛选索引)中。

筛选谓词中的数据转换运算符

如果筛选索引结果的筛选索引表达式中指定的比较运算符会导致隐式或显式数据转换,则转换发生在比较运算符的左边时,会出现错误。解决方法是在比较运算符的右边编写包含数据转换运算符(CAST 或 CONVERT)的筛选索引表达式。

下面的示例创建一个包含多种数据类型的表。

USE AdventureWorks;
GO
IF OBJECT_ID ('dbo.TestTable') IS NOT NULL
DROP TABLE dbo.TestTable;
GO
CREATE TABLE TestTable (a int, b varbinary(4));
GO

在下面的筛选索引定义中,列 b 隐式转换为整数数据类型,以便与常量 1 进行比较。因为转换发生在筛选谓词中运算符的左边,所以这会生成错误消息 10611。

USE AdventureWorks;
GO
IF EXISTS ( SELECT name from sys.indexes 
    WHERE name = N'TestTabIndex'
    AND object_id = OBJECT_ID (N'dbo.TestTable'))
DROP INDEX TestTabIndex on dbo.TestTable
GO
CREATE NONCLUSTERED INDEX TestTabIndex ON dbo.TestTable(a,b)
WHERE b = 1;
GO

解决方法是将右侧的常量转换为与列 b 的类型相同的类型,如下例所示:

CREATE INDEX TestTabIndex ON dbo.TestTable(a,b)
WHERE b = CONVERT(Varbinary(4), 1);
GO

将数据转换从比较运算符的左边移动到右边可能会改变转换的含义。在上例中,将 CONVERT 运算符添加到右边时,相应的比较从整数比较更改为 varbinary 比较。

引用依赖项

sys.sql_expression_dependencies 目录视图将筛选索引表达式中的每一列作为一个引用依赖项进行跟踪。不能删除、重命名或更改在筛选索引表达式中定义的表列的定义。

何时使用筛选索引

列中包含查询在 SELECT 语句中引用的定义完善的数据子集时,筛选索引很有用。以下是一些示例:

  • 仅包含少量非 NULL 值的稀疏列。

  • 包含多种类别的数据的异类列。

  • 包含多个范围的值(如美元金额、时间和日期)的列。

  • 由列值的简单比较逻辑定义的表分区。

如果索引中的行数与全表索引相比较少时,筛选索引减少的维护开销最为明显。如果筛选索引包含表中的大部分行,则与全表索引相比,其维护开销可能更高。在这种情况下,应使用全表索引而不是筛选索引。

筛选索引是针对一个表定义的,仅支持简单比较运算符。如果需要引用多个表或具有复杂逻辑的筛选表达式,则应创建视图。

筛选索引功能支持

一般情况下,数据库引擎和工具为筛选索引提供了与非聚集全表索引相同的支持,将筛选索引视为特殊类型的非聚集索引。下面的列表提供了有关对筛选索引提供完全支持、不提供支持或提供有限支持的工具和功能的说明。

  • ALTER INDEX 支持筛选索引。若要修改筛选索引表达式,请使用 CREATE INDEX WITH DROP_EXISTING。

  • 缺失索引功能不建议使用筛选索引。

  • 数据库引擎优化顾问在提供索引优化建议时会考虑筛选索引,并且可能会建议 is not null 筛选索引。

  • 联机索引操作支持筛选索引。

  • 表提示支持筛选索引,但有一些不适用于非筛选索引的限制。下一节将介绍这些内容。

查询注意事项

如果不论是否使用筛选索引,查询均选择相同的结果,则查询优化器会使用筛选索引。此前介绍的筛选索引 FIBillOfMaterialsWithEndDate 对以下两个查询有效。在第一个示例中,查询谓词与筛选索引谓词 WHERE EndDate IS NOT NULL 完全匹配。在第二个示例中,由于查询谓词包含索引中行的子集,所以它比筛选谓词具有更强的选择性。

SELECT ComponentID, StartDate FROM Production.BillOfMaterials
WHERE EndDate IS NOT NULL;
GO
SELECT ComponentID, StartDate FROM Production.BillOfMaterials
WHERE EndDate < '20000701';
GO

下一个查询也可使用 FIBillOfMaterialsWithEndDate。但是,由于存在其他决定查询开销的因素(如查询谓词的选择性),优化器可能不会选择筛选索引。如下例所示,可以通过将筛选索引用作查询提示强制优化器选择筛选索引。

SELECT ComponentID, StartDate FROM Production.BillOfMaterials
    WITH ( INDEX ( FIBillOfMaterialsWithEndDate ) )
WHERE EndDate IN ('20000825', '20000908', '20000918');
GO

如果查询可以返回不在筛选索引中的行,则查询优化器将不会使用筛选索引。例如,查询优化器将不考虑对下面的查询使用 FIBillOfMaterialsWithEndDate,因为查询可能返回 NULL EndDate 值和非 NULL ModifiedDate 值,这些值不能包含在 FIBillOfMaterialsWithEndDate 中(因为它只包含非 NULL EndDate 值)。

SELECT ComponentID, StartDate FROM Production.BillOfMaterials
WHERE EndDate IS NOT NULL OR ModifiedDate IS NOT NULL;
GO

如果筛选索引显式用作表提示且可能未包含所有查询结果,则查询优化器产生查询编译错误 8622。在下面的示例中,查询优化器产生错误 8622,因为 FIBillOfMaterialsWithEndDate 对于查询无效且它显式用作索引提示:

SELECT StartDate, ComponentID FROM Production.BillOfMaterials
    WITH ( INDEX ( FIBillOfMaterialsWithEndDate ) )
WHERE EndDate IS NOT NULL OR ModifiedDate IS NOT NULL;
GO

参数化查询

在某些情况下,参数化查询在编译时包含的信息不足以满足查询优化器选择筛选索引的需要。可能可以重写此查询以提供缺少的信息。在下面的示例中,查询优化器不考虑对 SELECT 语句使用筛选索引 FIBillOfMaterialsWithComponentID,因为 @p@q 的参数值在编译时未知。下面的查询示例运行时将 SHOWPLAN_XML 设置为 ON,以使您可以在 SHOWPLAN_XML 输出中查看参数化查询的不匹配的筛选索引。

USE AdventureWorks;
GO
IF EXISTS ( SELECT name FROM sys.indexes
    WHERE name = N'FIBillOfMaterialsWithComponentID'
    AND object_id = OBJECT_ID (N'Production.BillOfMaterials'))
DROP INDEX FIBillOfMaterialsWithComponentID
    ON Production.BillOfMaterials;
GO
CREATE NONCLUSTERED INDEX FIBillOfMaterialsWithComponentID
    ON Production.BillOfMaterials (ComponentID, StartDate, EndDate)
WHERE ComponentID IN (533, 324, 753);
GO
SET SHOWPLAN_XML ON;
GO
DECLARE @p AS INT, @q AS INT;
SET @p = 533;
SET @q = 324;
SELECT StartDate, ComponentID from Production.BillOfMaterials 
WHERE ComponentID = @p OR ComponentID = @q;
GO
SET SHOWPLAN_XML OFF;
GO

SHOWPLAN_XML 输出中的 UnmatchedIndexes 元素和 Parameterization 子元素指示筛选索引与查询不匹配。有关如何查看 SHOWPLAN_XML 输出的信息,请参阅 XML 显示计划

解决方法是修改此查询,使在参数化表达式不是筛选谓词的子集时查询结果为空。下面的查询说明了如何进行这种修改。通过将 ComponentID in (533, 324, 753) 表达式添加到 WHERE 子句,确保了此查询的结果是筛选谓词表达式的子集。通过这种修改,查询优化器可以考虑对下面的 SELECT 语句使用筛选索引 FIBillOfMaterialsWithComponentID。

USE AdventureWorks;
GO
SET SHOWPLAN_XML ON;
GO
DECLARE @p AS INT, @q AS INT;
SET @p = 533;
SET @q = 324;
SELECT StartDate, ComponentID FROM Production.BillOfMaterials
WHERE ComponentID in (533, 324, 753)
    AND (ComponentID = @p OR ComponentID = @q);
GO
SET SHOWPLAN_XML OFF;
GO

简单参数化

在大多数情况下,如果某查询计划包括筛选索引,查询优化器将不对该查询执行简单参数化(在 SQL Server 2005 中称为“自动参数化”)。对此类查询执行简单参数化可扩大可能参数值的范围,这样,筛选索引便不能保证查询结果的准确性。例如,如果 SELECT 语句的 WHERE 子句使用了在筛选索引的谓词中使用的列,则查询优化器可能不会执行简单参数化,这是因为查询计划中很可能会包括筛选索引。

如果适合,使用本节中所述的准则重写查询以确保筛选索引将涵盖该查询,也许能够参数化该查询。

使用键查找的查询

查询优化器可以执行键查找操作来检索筛选索引没有涵盖的剩余列,从而即使在某筛选索引不涵盖查询的情况下也可以使用该筛选索引。有关键查找的详细信息,请参阅Key Lookup Showplan 运算符。如果估计的键查找次数很少,则查询优化器可能会选择此方法。下面的查询使用索引提示强制查询处理器使用 FIBillOfMaterialsWithEndDate,同时对 EndDate 执行书签查找操作。对于查询谓词中的 EndDate > @date 比较,会执行键查找操作。

USE AdventureWorks;
GO
DECLARE @date AS DATE;
SET @date = '20000825'
SELECT ComponentID, StartDate, EndDate FROM Production.BillOfMaterials
WITH ( INDEX (FIBillOfMaterialsWithEndDate) )
WHERE EndDate > @date;
GO

请注意,EndDate > @Date 与筛选索引表达式 EndDate IS NOT NULL 不完全匹配。筛选索引对此参数化查询仍有效,因为它返回了由筛选索引表达式定义的行的子集。