2016-08-29 44 views
2

我目前正在試驗SQL Server中的篩選索引。我試圖通過將以下提示從BOL付諸實踐縮水過濾指數下跌:SQL Server中的篩選索引缺少謂詞不能按預期工作

在篩選索引表達式中的列並不需要在過濾索引定義,如果一個鍵或 包括列已過濾索引 表達式等同於查詢謂詞,並且查詢不會 返回已過濾索引表達式中的列與查詢 結果。

我已複製在一個小的測試腳本的問題: 我的表如下所示:

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中使用,因此它不包含在索引鍵或包含中。

但是,如果我運行查詢,我得到以下計劃: plan for query filters for operators

有在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; 

回答

2

這是優化的錯誤,特別是在它處理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; 

我說「不幸」,因爲這總是 - NULL列仍然會毫無意義地佔用行空間(因爲DATETIME是固定大小的數據類型)。即使如此,它可能比從聚簇索引查找中獲得額外的I/O好得多。此外,開銷可以通過compressing the index減少到幾乎沒有(甚至行壓縮)。

+0

嗨Jeroen,非常有幫助的答案,謝謝。現在我知道,我沒有做錯什麼。你是對的......包括列是一個更好的選擇,因爲過濾條件將值縮小爲NULL,並且我不會通過對其進行索引來獲得更好的查找。 –