拥有多个过滤索引有什么好处?

Ken*_*Ken 2 sql-server filtered-index

我有一个 SQL Server 数据库,偶然发现了一个表,其中包含一个名为 ActivityId 的唯一标识符主键,以及 StateCode (int) 和 ActivityTypeCode (int) 列以及许多其他列。这些表有 13M 行,ActivityTypeCode 有 94 个唯一值,行数从 1 到 4M。好的,这是奇怪的部分:我在 (StateCode, ActivityId) 上有 97 个不同的过滤索引,其中唯一的区别是 (ActivityTypeCode=( _some_value_ ))上的 where 子句。

在我看来,所有 97 个索引都可以删除并替换为仅 StateCode 上的一个索引(因为 ActivityID 是主键并且已经作为隐藏列包含在内)。我的问题是:有什么理由要保留多个过滤索引吗?他们能以某种方式减少死锁吗?我已经进行了一些测试,我认为没有任何理由拥有如此多的索引。想法?

[编辑] 基于下面的优秀答案,我做了一些测试,似乎过滤后的索引不会阻止简单查询的阻塞。我的索引是以下形式:

CREATE INDEX IX_StateCode_ActivityID_ActivityTypeCode_Filtered_0 ON dbo.Activities(StateCode,ActivityId) WHERE ActivityTypeCode = 0;
Run Code Online (Sandbox Code Playgroud)

如果我进入一个数据库连接并发出以下命令:

Begin transaction;
Update Activities set StateCode=888 where ActivityTypeCode=1
Run Code Online (Sandbox Code Playgroud)

然后进入第二个连接并发出以下命令:

Update Activities set StateCode=999 where ActivityTypeCode=3
Run Code Online (Sandbox Code Playgroud)

第二个连接挂起。似乎这些不同的索引并不能阻止阻塞,所以我看不出有任何理由保留它们。鉴于存在不存在的 ActivityTypeCode 值的索引,我怀疑它们都是由某种开发工具生成的。仅供参考:这是来自第三方供应商。

感谢大家的帮助。

[编辑 #2] 经过详细检查,并非所有索引都相同。下面是组合。请记住,ActivityID 是主键。

StateCode, ActivityID
StateCode, CreatedOn, ActivityID
StateCode, CreatedOn, StatusCode, ActivityID
StateCode, StatusCode, CreatedOn, Activity ID
Run Code Online (Sandbox Code Playgroud)

我不认为这会阻止我用单个索引替换它们 - 它只会比最初预期的大一点。

Ran*_*gen 5

在我看来,所有 97 个索引都可以删除并替换为仅 StateCode 上的一个索引(因为 ActivityID 是主键并且已经作为隐藏列包含在内)。我的问题是:有什么理由要保留多个过滤索引吗?

就像布伦特在评论中所说的那样,如果没有更多细节,这将很难给出一个好的答案。

我认为在不了解更多细节的情况下很难在这里得到一个好的答案,比如使用索引的查询。这可能是我开始研究的地方 - 开始查看每个过滤索引的索引使用统计信息,查看它们之间是否有任何差异,查看是否有一些 ActivityTypeCode 值未包含在过滤器中,等等

正如Jonathan Fite所说,如果查询使用过滤索引,它们可能是有益的。

如果您的查询实际上使用了过滤索引,并且特别是如果索引是查询的覆盖索引,那么它们可能是有益的。但我猜测(考虑到它们的数量)它们被添加是因为有人阅读了它们并认为这是一个好主意(tm)。有关 如何判断索引是否在查询中使用的指南,请参见此处


测试

这个答案只是一些测试和考虑,希望能让您更轻松地做出决定。根据您的工作负载,可能有所有这些过滤索引的原因,或者它们可能会囤积空间和资源而没有任何好处。我的数据量会比你的小,在SQL Server 2017. 一如既往的YMMV

尝试重新创建部分设置

我有一个 SQL Server 数据库,偶然发现了一个表,其中包含一个名为 ActivityId 的唯一标识符主键,以及 StateCode (int) 和 ActivityTypeCode (int) 列以及许多其他列。这些表有 13M 行,ActivityTypeCode 有 94 个唯一值,行数从 1 到 4M

CREATE TABLE dbo.Activities(ActivityId UNIQUEIDENTIFIER PRIMARY KEY NOT NULL  DEFAULT NEWSEQUENTIALID() , StateCode int, ActivityTypeCode int,val1 int, val2 int);

INSERT INTO dbo.Activities(StateCode,ActivityTypeCode,val1,val2)
SELECT 1,1,1,1;
-- 1 value

INSERT INTO dbo.Activities(StateCode,ActivityTypeCode,val1,val2)
SELECT TOP(4000000) 2,2,ROW_NUMBER() OVER(ORDER BY(SELECT NULL)) as rn1,ROW_NUMBER() OVER(ORDER BY(SELECT NULL)) as rn2

FROM master..spt_values spt1
CROSS APPLY master..spt_values spt2;
--4m values

INSERT INTO dbo.Activities(StateCode,ActivityTypeCode,val1,val2)
SELECT TOP(9000000) ROW_NUMBER() OVER(ORDER BY(SELECT NULL)) / 1000 % 300 as rn1,
                    ROW_NUMBER() OVER(ORDER BY(SELECT NULL)) / 1000 % 93 as rn2,
                    ROW_NUMBER() OVER(ORDER BY(SELECT NULL)) as rn3,
                    ROW_NUMBER() OVER(ORDER BY(SELECT NULL)) as rn4

FROM master..spt_values spt1
CROSS APPLY master..spt_values spt2
CROSS APPLY master..spt_values spt3;
-- 9M other values, ActivityTypeCode from 0 - 93
Run Code Online (Sandbox Code Playgroud)

我在 (StateCode, ActivityId) 上有 97 个不同的过滤索引,其中唯一的区别是 (ActivityTypeCode=(_some_value_)) 上的 where 子句。

我选择了 94 个索引,用于 94 个不同的值 ActivityTypeCode

SELECT 'CREATE INDEX IX_StateCode_ActivityID_ActivityTypeCode_Filtered_'+CAST((rn-1) as varchar(2)) 
        + ' ON dbo.Activities(StateCode,ActivityId) INCLUDE(ActivityTypeCode)'
        + ' WHERE ActivityTypeCode = '+CAST((rn-1) as varchar(2)) +';'
FROM
(
SELECT TOP(94) ROW_NUMBER() OVER( ORDER BY (SELECT NULL)) as rn
FROM master..spt_values 
) as a
Run Code Online (Sandbox Code Playgroud)

使用的空间

EXEC sp_spaceused 'dbo.Activities'

name        rows                    reserved    data        index_size  unused
Activities  13000001                994736 KB   527920 KB   437488 KB   29328 KB
Run Code Online (Sandbox Code Playgroud)

过滤索引测试

在您的问题中,您没有指定是否ActivityTypeCode包含在过滤索引中。如果不包括它们,则只会使用精确的过滤器匹配。更多关于这里

没有 OPTION RECOMPILE 的参数化查询

使用不带OPTION(RECOMPILE)的参数化查询时,sql server 在运行时将不知道该参数。

这意味着您将无法使用过滤后的索引。如果我们能够使用过滤索引,我们将无法重用具有不同参数的计划

--If your queries are parameterized 
EXEC SP_EXECUTESQL N'SELECT StateCode,ActivityId 
                     FROM dbo.Activities 
                     WHERE ActivityTypeCode = @P1 and StateCode = @P2',N'@P1 INT, @P2 int',@P1=1,@P2=2
Run Code Online (Sandbox Code Playgroud)

在此处输入图片说明

对于如下存储过程也是如此:

CREATE PROCEDURE dbo.bla(@P1 int, @P2 int)
AS
BEGIN
SELECT StateCode,ActivityId 
FROM dbo.Activities 
WHERE ActivityTypeCode = @P1 and StateCode = @P2

END
EXEC dbo.bla @P1=1, @P2=2;
Run Code Online (Sandbox Code Playgroud)

带有 OPTION RECOMPILE 的参数化查询

EXEC SP_EXECUTESQL N'SELECT StateCode,ActivityId 
                     FROM dbo.Activities 
                     WHERE ActivityTypeCode = @P1 and StateCode = @P2
                     OPTION(RECOMPILE)',N'@P1 INT, @P2 int',@P1=1,@P2=2
Run Code Online (Sandbox Code Playgroud)

如前所述,OPTION(RECOMPILE)使我们能够在创建计划时使用过滤索引。 在此处输入图片说明

您可以将相同OPTION(RECOMPILE)的内容添加到之前讨论的过程中。

您还可以使用动态 SQL 将过滤的一部分作为文字传递,将过滤的一部分作为参数化查询传递。更多关于这里

估计

使用过滤索引的计划的一个问题是查询估计值远高于实际值:

在此处输入图片说明

您可以通过创建/更改过滤索引来解决这些估计值,以便将 ActivityTypeCode 作为关键列之一而不是 ActivityId。

CREATE INDEX IX_StateCode_ActivityID_ActivityTypeCode_Filtered_1_B 
ON dbo.Activities(StateCode,ActivityTypeCode) 
INCLUDE(ActivityId) 
WHERE ActivityTypeCode = 1;
Run Code Online (Sandbox Code Playgroud)

这仅在也过滤时才成立StateCode

我无法创建自定义统计信息或让 sql server 使用自定义统计信息来解决此问题。可能有一种方法可以做到这一点而不必更改索引


过滤索引 < - > 常规索引

在我看来,所有 97 个索引都可以删除并替换为仅 StateCode 上的一个索引(因为 ActivityID 是主键并且已经作为隐藏列包含在内)。我的问题是:有什么理由要保留多个过滤索引吗?

假设

让我们假设查询可以利用过滤的索引并且ActivityTypeCode包含在索引中。

索引

OP 提议的索引

CREATE INDEX IX_StateCode
ON dbo.Activities(StateCode);
Run Code Online (Sandbox Code Playgroud)

这个索引对我来说似乎很奇怪,如果你只过滤StateCode,为什么过滤的索引ActivityTypeCode会存在?

这就是为什么我们将在此示例中关注其他两个索引的原因:

CREATE INDEX IX_StateCode_ActivityTypeCode
ON dbo.Activities(StateCode,ActivityTypeCode);


CREATE INDEX IX_ActivityTypeCode_StateCode
ON dbo.Activities(ActivityTypeCode,StateCode);
Run Code Online (Sandbox Code Playgroud)

使用的空间

EXEC sp_spaceused 'dbo.Activities'

name        rows                    reserved    data        index_size  unused
Activities  13000001                1819584 KB  527936 KB   1260992 KB  30656 KB
Run Code Online (Sandbox Code Playgroud)

测试

过滤StateCode&ActivityTypeCode

运行之前使用过滤索引的查询:

EXEC SP_EXECUTESQL N'SELECT StateCode,ActivityId 
                     FROM dbo.Activities 
                     WHERE ActivityTypeCode = @P1 and StateCode = @P2
                     OPTION(RECOMPILE)',N'@P1 INT, @P2 int',@P1=1,@P2=2
Run Code Online (Sandbox Code Playgroud)

使用了新的指数并且估计是正确的。

在此处输入图片说明

我们之前看到,对于这些特定参数,过滤索引的估计值约为 400 万行。这可以通过创建新索引来规避

仅过滤 ActivityTypeCode

当只过滤ActivityTypeCode它开始变得有趣时

EXEC SP_EXECUTESQL N'SELECT StateCode,ActivityId 
                     FROM dbo.Activities 
                     WHERE ActivityTypeCode = @P1
                     OPTION(RECOMPILE)',N'@P1 INT',@P1=1
Run Code Online (Sandbox Code Playgroud)

使用非过滤索引

在此处输入图片说明

虽然理论上,可以扫描整个过滤索引而无需应用任何过滤: 在此处输入图片说明

并且两个估计的行都是正确的。

即使对应该返回+4M行的参数进行@P1=2过滤,也会选择未过滤的索引 + 搜索。

在此处输入图片说明

这是由于估计行数和索引查找 <-> 扫描临界点。

添加4M额外行时,选择扫描。

在此处输入图片说明

如果归结为这一点,我可以删除过滤的索引并使用上面指定的常规索引。但同样,这是基于对自己的测试数据的一次查询。需要更多详细信息才能获得与您的数据集和工作负载更接近的答案。

测试告诉我们什么?

测试结果仅向我们提供有关我自己的测试和我自己的查询的信息。

一些要点是:

  • 过滤索引的大小不会高于常规索引。 管理这些会更难,如果添加没有过滤索引的新值怎么办
  • 有不能使用过滤索引的情况,比如参数化
  • 如果您使用的是文字/OPTION(RECOMPILE)您将能够使用过滤索引并可能以重新编译为代价获得更好的查询计划
  • 您应该在过滤索引中包含该列并注意错误的估计

剩下的就由你来想办法了。