【问题标题】:Filtered Index in SQL Server missing predicate does not work as expectedSQL Server 中缺少谓词的筛选索引无法按预期工作
【发布时间】:2016-08-29 07:25:00
【问题描述】:

我目前正在 SQL Server 中试验过滤索引。我试图通过将BOL 的以下提示付诸实践来缩小过滤索引:

过滤索引表达式中的列不需要是键或 如果过滤索引,则在过滤索引定义中包含列 表达式等价于查询谓词,而查询不 使用查询返回过滤后的索引表达式中的列 结果。

我在一个小测试脚本中重现了这个问题: 我的表格如下所示:

CREATE TABLE #test
(
    ID  BIGINT NOT NULL IDENTITY(1,1),
    ARCHIVEDATE DATETIME NULL,
    CLOSINGDATE DATETIME NULL,
    OBJECTTYPE INTEGER NOT NULL,
    ACTIVE BIT NOT NULL,
    FILLER1 CHAR(255) DEFAULT 'just a filler',
    FILLER2 CHAR(255) DEFAULT 'just a filler',
    FILLER3 CHAR(255) DEFAULT 'just a filler',
    FILLER4 CHAR(255) DEFAULT 'just a filler',
    FILLER5 CHAR(255) DEFAULT 'just a filler',
    CONSTRAINT test_pk PRIMARY KEY CLUSTERED (ID ASC)
);

我需要优化以下查询:

SELECT  
    COUNT(*) 
FROM    
    #test 
WHERE       
        ARCHIVEDATE IS NULL 
    AND CLOSINGDATE IS NOT NULL 
    AND ISNULL(ACTIVE,1) != 0

因此我建立了以下过滤索引:

CREATE NONCLUSTERED INDEX idx_filterTest ON #test (/*ARCHIVEDATE ASC,*/CLOSINGDATE ASC) INCLUDE (ACTIVE) WHERE ARCHIVEDATE IS NULL;

ARCHIVEDATE 已经在过滤器中,不会在 SELECT 中使用,因此它不包含在索引键或包含中。

但是,如果我运行查询,我会得到以下计划:

在 ARCHIVEDATE 的聚集索引中有一个键查找。为什么呢?我已经在 SQL Server 2008 和 SQL Server 2016 上重现了这种行为。

如果我在键中使用 ARCHIVEDATE 创建索引,我只需进行一次索引查找即可。所以在我看来,BOL 中的这一段并不总是适用。

这是我的完整重现脚本:

--DROP TABLE #test;
CREATE TABLE #test
(
    ID  BIGINT NOT NULL IDENTITY(1,1),
    ARCHIVEDATE DATETIME NULL,
    CLOSINGDATE DATETIME NULL,
    OBJECTTYPE INTEGER NOT NULL,
    ACTIVE BIT NOT NULL,
    FILLER1 CHAR(255) DEFAULT 'just a filler',
    FILLER2 CHAR(255) DEFAULT 'just a filler',
    FILLER3 CHAR(255) DEFAULT 'just a filler',
    FILLER4 CHAR(255) DEFAULT 'just a filler',
    FILLER5 CHAR(255) DEFAULT 'just a filler',
    CONSTRAINT test_pk PRIMARY KEY CLUSTERED (ID ASC)
);



INSERT INTO #test
(ARCHIVEDATE, CLOSINGDATE, OBJECTTYPE, ACTIVE)
SELECT TOP 200
    NULL,
    dates.calcDate,
    4711,
    dates.number%2
FROM
    (
        SELECT
            /* Erzeugen des Datums durch Addieren der jeweiligen Sequenznummer zum StartDate */
            DATEADD(DAY, seq.number, '20120101') AS calcDate, number
        FROM
        (
            /* Abfrage zur Erstellung einer Nummernsequenz von 0 bis 9999. Dient als Basis zur Aufbereitung aller Datumswerte im Zeitraum. Die Sequenz reicht für einen Zeitraum von ca. 30 Jahren aus. */
            SELECT
                a.num * 1000 + b.num * 100 + c.num * 10 + d.num AS number
            FROM
                        ( SELECT 0 AS num UNION ALL SELECT 1 AS num UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) a
            CROSS JOIN  ( SELECT 0 AS num UNION ALL SELECT 1 AS num UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) b
            CROSS JOIN  ( SELECT 0 AS num UNION ALL SELECT 1 AS num UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) c
            CROSS JOIN  ( SELECT 0 AS num UNION ALL SELECT 1 AS num UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) d
        ) seq 
        WHERE
            /* Einschränkung der Nummernsequenz auf die Anzahl der Tage im gewünschten Aufbereitungszeitraum */
            seq.number <= 5000
    ) dates
ORDER BY
    dates.number
;



INSERT INTO #test
(ARCHIVEDATE, CLOSINGDATE, OBJECTTYPE, ACTIVE)
SELECT TOP 1000
    dates.calcDate + 3,
    dates.calcDate,
    4711,
    dates.number%2
FROM
    (
        SELECT
            /* Erzeugen des Datums durch Addieren der jeweiligen Sequenznummer zum StartDate */
            DATEADD(DAY, seq.number, '20120101') AS calcDate, number
        FROM
        (
            /* Abfrage zur Erstellung einer Nummernsequenz von 0 bis 9999. Dient als Basis zur Aufbereitung aller Datumswerte im Zeitraum. Die Sequenz reicht für einen Zeitraum von ca. 30 Jahren aus. */
            SELECT
                a.num * 1000 + b.num * 100 + c.num * 10 + d.num AS number
            FROM
                        ( SELECT 0 AS num UNION ALL SELECT 1 AS num UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) a
            CROSS JOIN  ( SELECT 0 AS num UNION ALL SELECT 1 AS num UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) b
            CROSS JOIN  ( SELECT 0 AS num UNION ALL SELECT 1 AS num UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) c
            CROSS JOIN  ( SELECT 0 AS num UNION ALL SELECT 1 AS num UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) d
        ) seq 
        WHERE
            /* Einschränkung der Nummernsequenz auf die Anzahl der Tage im gewünschten Aufbereitungszeitraum */
            seq.number <= 5000
    ) dates
ORDER BY
    dates.number
;


INSERT INTO #test
(ARCHIVEDATE, CLOSINGDATE, OBJECTTYPE, ACTIVE)
SELECT TOP 100000
    dates.calcDate,
    NULL,
    4711,
    dates.number%2
FROM
    (
        SELECT
            /* Erzeugen des Datums durch Addieren der jeweiligen Sequenznummer zum StartDate */
            DATEADD(DAY, seq.number, '20120101') AS calcDate, number
        FROM
        (
            /* Abfrage zur Erstellung einer Nummernsequenz von 0 bis 9999. Dient als Basis zur Aufbereitung aller Datumswerte im Zeitraum. Die Sequenz reicht für einen Zeitraum von ca. 30 Jahren aus. */
            SELECT
                a.num * 1000 + b.num * 100 + c.num * 10 + d.num AS number
            FROM
                        ( SELECT 0 AS num UNION ALL SELECT 1 AS num UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) a
            CROSS JOIN  ( SELECT 0 AS num UNION ALL SELECT 1 AS num UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) b
            CROSS JOIN  ( SELECT 0 AS num UNION ALL SELECT 1 AS num UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) c
            CROSS JOIN  ( SELECT 0 AS num UNION ALL SELECT 1 AS num UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) d
        ) seq 
        WHERE
            /* Einschränkung der Nummernsequenz auf die Anzahl der Tage im gewünschten Aufbereitungszeitraum */
            seq.number <= 5000
    ) dates
ORDER BY
    dates.number
;


--DROP INDEX idx_filterTest ON #test;
--CREATE NONCLUSTERED INDEX idx_filterTest ON #test (ARCHIVEDATE ASC,CLOSINGDATE ASC) INCLUDE (ACTIVE) WHERE ARCHIVEDATE IS NULL;
CREATE NONCLUSTERED INDEX idx_filterTest ON #test (/*ARCHIVEDATE ASC,*/CLOSINGDATE ASC) INCLUDE (ACTIVE) WHERE ARCHIVEDATE IS NULL;



SELECT  
    COUNT(*) 
FROM    
    #test 
WHERE       
        ARCHIVEDATE IS NULL 
    AND CLOSINGDATE IS NOT NULL 
    AND ISNULL(ACTIVE,1) != 0;

【问题讨论】:

    标签: sql-server indexing filtered-index


    【解决方案1】:

    这是优化器中的一个错误,特别是它处理IS NULL 过滤器的方式。这是一个更简单的复制:

    CREATE TABLE #T(ID INT IDENTITY PRIMARY KEY, X INT);
    INSERT #T(X) SELECT TOP(10000) message_id FROM sys.messages WHERE message_id <> 1;
    INSERT #T(X) VALUES (1);
    INSERT #T(X) VALUES (NULL);
    CREATE INDEX IX_#T_X_null ON #T(ID) WHERE X IS NULL;
    CREATE INDEX IX_#T_X_1 ON #T(ID) WHERE X = 1;
    

    很明显,IX_#T_X_null 涵盖了以下查询:

    SELECT MIN(ID) FROM #T WHERE X IS NULL;
    

    优化器确实选择了它,但我们得到了一个执行计划,其中插入了多余的聚集索引查找。但是:

    SELECT MIN(ID) FROM #T WHERE X = 1;
    

    现在我们得到一个没有聚集索引查找的查询。当涉及IS NULL 时,优化器似乎认识到过滤索引适用,但随后无法将条件传播到后面的步骤。如果我们将列包含在索引中,我们可以清楚地看到这一点:

    CREATE INDEX IX_#T_X_null ON #T(ID, X) WHERE X IS NULL;
    

    如果您现在比较 WHERE X = 1WHERE X IS NULL 查询的执行计划,您会看到在 X IS NULL 的情况下,优化器在索引扫描中添加了一个谓词,但它没有这样做X = 1.

    再深入一点,通过这个特定的设置,你会发现这是一个known issue, already reported on Connect。然而,根据微软的说法,“这实际上不是一个错误,而是一个已知的功能差距”(我认为这在技术上是正确的,因为结果不是不正确的,它只是表现得不够好)。此外,“这现在是 SQL Server 未来版本的活动 DCR”,但那是 6 年前的事了,票已关闭为“不会修复”——所以不要屏住呼吸。

    不幸的是,解决方法确实是将该列包含在索引中——我会将其设为包含列而不是键,因为这会增加非叶级别的开销:

    CREATE NONCLUSTERED INDEX idx_filterTest ON #test (CLOSINGDATE ASC)
    INCLUDE (ACTIVE, ARCHIVEDATE) 
    WHERE ARCHIVEDATE IS NULL;
    

    我说“不幸”是因为这个 always-NULL 列仍然会毫无意义地占用行空间(因为 DATETIME 是一个固定大小的数据类型)。即便如此,它可能比从聚集索引查找中获得额外的 I/O 好几英里。此外,compressing the index 可以将开销几乎减少到零(甚至行压缩也可以)。

    【讨论】:

    • 您好 Jeroen,感谢您提供的非常有帮助的答案。现在我知道了,我没有做错什么。你是对的......包含列是一个更好的选择,因为过滤条件将值缩小到 NULL,并且我不会通过对其进行索引来获得任何更好的搜索。
    猜你喜欢
    • 1970-01-01
    • 2016-07-01
    • 1970-01-01
    • 2010-12-20
    • 1970-01-01
    • 1970-01-01
    • 2020-07-02
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多