Индекс по столбцу Постоянный вычисляемый нуждается в поиске ключа, чтобы получить столбцы в вычисляемом выражении

24

У меня есть постоянный вычисляемый столбец в таблице, который просто состоит из сцепленных столбцов, например

CREATE TABLE dbo.T 
(   
    ID INT IDENTITY(1, 1) NOT NULL CONSTRAINT PK_T_ID PRIMARY KEY,
    A VARCHAR(20) NOT NULL,
    B VARCHAR(20) NOT NULL,
    C VARCHAR(20) NOT NULL,
    D DATE NULL,
    E VARCHAR(20) NULL,
    Comp AS A + '-' + B + '-' + C PERSISTED NOT NULL 
);

Это Compне уникально, и D является действительной датой каждой комбинации A, B, C, поэтому я использую следующий запрос, чтобы получить конечную дату для каждой A, B, C(в основном, следующую начальную дату для того же значения Comp):

SELECT  t1.ID,
        t1.Comp,
        t1.D,
        D2 = (  SELECT  TOP 1 t2.D
                FROM    dbo.T t2
                WHERE   t2.Comp = t1.Comp
                AND     t2.D > t1.D
                ORDER BY t2.D
            )
FROM    dbo.T t1
WHERE   t1.D IS NOT NULL -- DON'T CARE ABOUT INACTIVE RECORDS
ORDER BY t1.Comp;

Затем я добавил индекс в вычисляемый столбец, чтобы помочь в этом запросе (а также других):

CREATE NONCLUSTERED INDEX IX_T_Comp_D ON dbo.T (Comp, D) WHERE D IS NOT NULL;

Однако план запроса меня удивил. Я бы подумал, что, поскольку у меня есть предложение where, в котором указано, что D IS NOT NULLя сортирую Comp, а не ссылаюсь ни на один столбец за пределами индекса, индекс для вычисляемого столбца можно использовать для сканирования t1 и t2, но я увидел кластеризованный индекс сканирования.

введите описание изображения здесь

Поэтому я заставил использовать этот индекс, чтобы увидеть, дает ли он лучший план:

SELECT  t1.ID,
        t1.Comp,
        t1.D,
        D2 = (  SELECT  TOP 1 t2.D
                FROM    dbo.T t2
                WHERE   t2.Comp = t1.Comp
                AND     t2.D > t1.D
                ORDER BY t2.D
            )
FROM    dbo.T t1 WITH (INDEX (IX_T_Comp_D))
WHERE   t1.D IS NOT NULL
ORDER BY t1.Comp;

Который дал этот план

введите описание изображения здесь

Это показывает, что используется поиск ключа, подробности которого:

введите описание изображения здесь

Теперь согласно документации SQL-сервера:

Вы можете создать индекс для вычисляемого столбца, который определен с помощью детерминированного, но неточного выражения, если столбец помечен как PERSISTED в операторе CREATE TABLE или ALTER TABLE. Это означает, что компонент Database Engine сохраняет вычисленные значения в таблице и обновляет их при обновлении любых других столбцов, от которых зависит вычисляемый столбец. Компонент Database Engine использует эти постоянные значения при создании индекса для столбца и при ссылке на индекс в запросе. Этот параметр позволяет создать индекс для вычисляемого столбца, когда компонент Database Engine не может с точностью доказать, является ли функция, которая возвращает выражения вычисляемого столбца, в частности, функция CLR, созданная в .NET Framework, детерминистической и точной.

Поэтому, если, как говорят документы, « компонент Database Engine хранит вычисленные значения в таблице» , а значение также сохраняется в моем индексе, почему для поиска A, B и C требуется поиск ключей, если на них нет ссылок в запрос вообще? Я предполагаю, что они используются для вычисления Comp, но почему? Кроме того, почему запрос может использовать индекс включен t2, но не включен t1?

Запросы и DDL на скрипте SQL

NB Я пометил SQL Server 2008, потому что это версия, на которой моя основная проблема, но я также получаю такое же поведение в 2012 году.

GarethD
источник

Ответы:

20

Почему поиск ключей необходим для получения A, B и C, когда на них вообще нет ссылок в запросе? Я предполагаю, что они используются для вычисления Comp, но почему?

Колонны A, B, and C имеют ссылки в плане запроса - они используются искать на T2.

Кроме того, почему запрос может использовать индекс на t2, но не на t1?

Оптимизатор решил, что сканирование кластеризованного индекса дешевле, чем сканирование отфильтрованного некластеризованного индекса, а затем выполнение поиска для получения значений для столбцов A, B и C.

объяснение

Реальный вопрос заключается в том, почему оптимизатор чувствовал необходимость извлекать A, B и C для поиска по индексу вообще. Мы ожидаем, что он прочитает Compстолбец с использованием сканирования некластеризованного индекса, а затем выполнит поиск по тому же индексу (псевдоним T2), чтобы найти запись Top 1.

Оптимизатор запросов расширяет ссылки на вычисляемые столбцы до начала оптимизации, чтобы дать ему возможность оценить затраты на различные планы запросов. Для некоторых запросов расширение определения вычисляемого столбца позволяет оптимизатору находить более эффективные планы.

Когда оптимизатор обнаруживает коррелированный подзапрос, он пытается «развернуть» его до формы, которую он находит более понятной. Если он не может найти более эффективное упрощение, он переписывает коррелированный подзапрос как применение (коррелированное соединение):

Применить переписать

Просто так получилось, что это применение разворачивания переводит логическое дерево запросов в форму, которая плохо работает с нормализацией проекта (более поздняя стадия, которая, среди прочего, ищет соответствие общих выражений вычисляемым столбцам).

В вашем случае способ написания запроса взаимодействует с внутренними деталями оптимизатора, так что определение расширенного выражения не сопоставляется с вычисляемым столбцом, и в результате вы получаете запрос, который ссылается на столбцы, A, B, and Cа не на вычисляемый столбец Comp. Это коренная причина.

Временное решение

Одна из идей, чтобы обойти этот побочный эффект, состоит в том, чтобы написать запрос как заявку вручную:

SELECT
    T1.ID,
    T1.Comp,
    T1.D,
    CA.D2
FROM dbo.T AS T1
CROSS APPLY
(  
    SELECT TOP (1)
        D2 = T2.D
    FROM dbo.T AS T2
    WHERE
        T2.Comp = T1.Comp
        AND T2.D > T1.D
    ORDER BY
        T2.D ASC
) AS CA
WHERE
    T1.D IS NOT NULL -- DON'T CARE ABOUT INACTIVE RECORDS
ORDER BY
    T1.Comp;

К сожалению, этот запрос не будет использовать отфильтрованный индекс, как мы надеемся. Проверка на неравенство в столбце Dвнутри применения отклоняет NULLs, поэтому очевидно, что избыточный предикат WHERE T1.D IS NOT NULLоптимизирован.

Без этого явного предиката логика сопоставления фильтрованного индекса решает, что она не может использовать фильтрованный индекс. Существует несколько способов обойти этот второй побочный эффект, но, вероятно, проще всего изменить кросс-применение на внешнее применение (отражая логику перезаписи оптимизатора, выполненного ранее для коррелированного подзапроса):

SELECT
    T1.ID,
    T1.Comp,
    T1.D,
    CA.D2
FROM dbo.T AS T1
OUTER APPLY
(  
    SELECT TOP (1)
        D2 = T2.D
    FROM dbo.T AS T2
    WHERE
        T2.Comp = T1.Comp
        AND T2.D > T1.D
    ORDER BY
        T2.D ASC
) AS CA
WHERE
    T1.D IS NOT NULL -- DON'T CARE ABOUT INACTIVE RECORDS
ORDER BY
    T1.Comp;

Теперь оптимизатору не нужно использовать саму команду «переписать» (поэтому сопоставление вычисляемых столбцов работает должным образом), и предикат также не оптимизирован, поэтому отфильтрованный индекс можно использовать для обеих операций доступа к данным, а для поиска используется Compстолбец. с обеих сторон:

План внешнего применения

Обычно это предпочтительнее добавления A, B и C в качестве INCLUDEdстолбцов в фильтруемом индексе, поскольку оно устраняет основную причину проблемы и не требует ненужного расширения индекса.

Постоянные вычисляемые столбцы

В качестве примечания, нет необходимости отмечать вычисляемый столбец как PERSISTED, если вы не возражаете повторить его определение в CHECKограничении:

CREATE TABLE dbo.T 
(   
    ID integer IDENTITY(1, 1) NOT NULL,
    A varchar(20) NOT NULL,
    B varchar(20) NOT NULL,
    C varchar(20) NOT NULL,
    D date NULL,
    E varchar(20) NULL,
    Comp AS A + '-' + B + '-' + C,

    CONSTRAINT CK_T_Comp_NotNull
        CHECK (A + '-' + B + '-' + C IS NOT NULL),

    CONSTRAINT PK_T_ID 
        PRIMARY KEY (ID)
);

CREATE NONCLUSTERED INDEX IX_T_Comp_D
ON dbo.T (Comp, D) 
WHERE D IS NOT NULL;

В этом случае вычисляемый столбец требуется только PERSISTEDв том случае, если вы хотите использовать NOT NULLограничение или ссылаться на Compстолбец напрямую (вместо того, чтобы повторять его определение) в CHECKограничении.

Пол Уайт говорит, что GoFundMonica
источник
2
+1 Кстати, я наткнулся на еще один случай излишнего поиска, глядя на это, что вы можете (или не можете) найти интерес. SQL Fiddle .
Мартин Смит,
@MartinSmith Да, это интересно. Другое общее правило rewrite ( FOJNtoLSJNandLASJN), которое приводит к тому, что все работает не так, как мы надеемся, и оставляет ненужную информацию (BaseRow / Checksums), которая полезна в некоторых типах планов (например, курсоры), но не нужна здесь.
Пол Уайт говорит GoFundMonica
Ах, Chkэто контрольная сумма! Спасибо, я не был уверен в этом. Первоначально я думал, что это может быть связано с проверочными ограничениями.
Мартин Смит
6

Хотя это может быть несколько случайным из-за искусственного характера ваших тестовых данных, но, как вы упомянули SQL 2012, я попытался переписать:

SELECT  ID,
        Comp,
        D,
        D2 = LEAD(D) OVER(PARTITION BY COMP ORDER BY D)
FROM    dbo.T 
WHERE   D IS NOT NULL
ORDER BY Comp;

Это дало хороший недорогой план с использованием вашего индекса и со значительно меньшим чтением, чем другие варианты (и те же результаты для ваших тестовых данных).

Plan Explorer стоит для четырех вариантов: Оригинал;  оригинал с подсказкой;  внешнее применение и привести

Я подозреваю, что ваши реальные данные более сложны, поэтому могут быть некоторые сценарии, когда этот запрос ведет себя семантически иначе, чем ваш, но иногда он показывает, что новые функции могут иметь реальное значение.

Я экспериментировал с некоторыми более разнообразными данными и обнаружил, что некоторые сценарии совпадают, а некоторые нет:

--Example 1: results matched
TRUNCATE TABLE dbo.t

-- Generate some more interesting test data
;WITH cte AS
(
SELECT TOP 1000 ROW_NUMBER() OVER ( ORDER BY ( SELECT 1 ) ) rn
FROM master.sys.columns c1
    CROSS JOIN master.sys.columns c2
    CROSS JOIN master.sys.columns c3
)
INSERT T (A, B, C, D)
SELECT  'A' + CAST( a.rn AS VARCHAR(5) ),
        'B' + CAST( a.rn AS VARCHAR(5) ),
        'C' + CAST( a.rn AS VARCHAR(5) ),
        DATEADD(DAY, a.rn + b.rn, '1 Jan 2013')
FROM cte a
    CROSS JOIN cte b
WHERE a.rn % 3 = 0
 AND b.rn % 5 = 0
ORDER BY 1, 2, 3
GO


-- Original query
SELECT  t1.ID,
        t1.Comp,
        t1.D,
        D2 = (  SELECT  TOP 1 D
                FROM    dbo.T t2
                WHERE   t2.Comp = t1.Comp
                AND     t2.D > t1.D
                ORDER BY D
            )
INTO #tmp1
FROM    dbo.T t1 
WHERE   t1.D IS NOT NULL
ORDER BY t1.Comp;
GO

SELECT  ID,
        Comp,
        D,
        D2 = LEAD(D) OVER(PARTITION BY COMP ORDER BY D)
INTO #tmp2
FROM    dbo.T 
WHERE   D IS NOT NULL
ORDER BY Comp;
GO


-- Checks ...
SELECT * FROM #tmp1
EXCEPT
SELECT * FROM #tmp2

SELECT * FROM #tmp2
EXCEPT
SELECT * FROM #tmp1


Example 2: results did not match
TRUNCATE TABLE dbo.t

-- Generate some more interesting test data
;WITH cte AS
(
SELECT TOP 1000 ROW_NUMBER() OVER ( ORDER BY ( SELECT 1 ) ) rn
FROM master.sys.columns c1
    CROSS JOIN master.sys.columns c2
    CROSS JOIN master.sys.columns c3
)
INSERT T (A, B, C, D)
SELECT  'A' + CAST( a.rn AS VARCHAR(5) ),
        'B' + CAST( a.rn AS VARCHAR(5) ),
        'C' + CAST( a.rn AS VARCHAR(5) ),
        DATEADD(DAY, a.rn, '1 Jan 2013')
FROM cte a

-- Add some more data
INSERT dbo.T (A, B, C, D)
SELECT A, B, C, D 
FROM dbo.T
WHERE DAY(D) In ( 3, 7, 9 )


INSERT dbo.T (A, B, C, D)
SELECT A, B, C, DATEADD( day, 1, D )
FROM dbo.T
WHERE DAY(D) In ( 12, 13, 17 )


SELECT * FROM #tmp1
EXCEPT
SELECT * FROM #tmp2

SELECT * FROM #tmp2
EXCEPT
SELECT * FROM #tmp1

SELECT * FROM #tmp2
INTERSECT
SELECT * FROM #tmp1


select * from #tmp1
where comp = 'A2-B2-C2'

select * from #tmp2
where comp = 'A2-B2-C2'
wBob
источник
1
Ну, он использует индекс, но только до определенного момента. Если compэто не вычисляемый столбец, вы не видите сортировку.
Мартин Смит,
Спасибо. Мой реальный сценарий не намного сложнее, и LEADфункция работала именно так, как я хотел бы на своем локальном экземпляре 2012 Express. К сожалению, это незначительное неудобство для меня еще не считалось достаточной причиной для обновления производственных серверов ...
GarethD
-1

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

Как видно из сканирования кластерного индекса (t2), предикат используется для определения необходимых строк, которые должны быть возвращены (из-за условия):

введите описание изображения здесь

Когда индекс был добавлен, независимо от того, был ли он определен оператором WITH или нет, план выполнения стал следующим:

введите описание изображения здесь

Как мы видим, сканирование кластеризованного индекса заменено сканированием индекса. Как мы видели выше, SQL Server использует исходные столбцы вычисляемого столбца для выполнения сопоставления вложенного запроса. Во время сканирования кластерного индекса все эти значения могут быть получены одновременно (никаких дополнительных операций не требуется). Когда индекс был добавлен, фильтрация необходимых строк из таблицы (в главном выборе) выполняется в соответствии с индексом, но значения исходных столбцов для вычисляемого столбца compвсе еще необходимо получить (последняя операция Nested Loop) ,

введите описание изображения здесь

Из-за этого используется операция Key Lookup - для получения данных исходных столбцов вычисляемого.

PS Похоже, ошибка в SQL Server.

Sandr
источник