2016-08-29 4 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 = 1 и WHERE X IS NULL запросов, вы увидите, что в случае X IS NULL, оптимизатор добавляет предикат в индексный сканер, который он не делает с X = 1.

И немного углубившись в эту конкретную настройку, вы можете найти это known issue, already reported on Connect. Однако, по словам Microsoft, «на самом деле это не ошибка, а известный пробел в функциональности» (что, я полагаю, технически верно, поскольку результаты не являются неправильными, они просто не работают так хорошо, как могли). Кроме того, «теперь это активный DCR для будущего выпуска SQL Server», но это было 6 лет назад, и билет закрыт как «Не исправить» - так что не задерживайте дыхание.

К сожалению, обходной путь действительно включить столбец в индексе - я бы сделать его включенную колонку, а не ключ, так что добавляет накладные расходы на уровни без листьев:

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

Я говорю «к сожалению», потому что этот столбец всегда - NULL по-прежнему бессмысленно занимает пространство строк (так как DATETIME - это тип данных фиксированного размера). Тем не менее, это, вероятно, намного лучше, чем получение дополнительных операций ввода-вывода из кластеризованных поисковых запросов. Кроме того, накладные расходы могут быть уменьшены почти до нуля на compressing the index (даже сжатие строк будет выполнено).

+0

Hi Jeroen, спасибо за очень полезный ответ. Теперь я знаю, что я не сделал что-то не так. Вы правы ... в том числе столбец - лучший вариант, поскольку условие фильтра сужает значения до NULL, и я не буду лучше искать его, индексируя его. –