Індекс на персистовані обчислені стовпці потребує пошуку ключа, щоб отримати стовпці в обчисленому виразі


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;

План запитів, однак, мене здивував. Я б подумав, що оскільки у мене є пункт де зазначено, що 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-сервер:

Ви можете створити індекс на обчисленому стовпчику, який визначений детермінованим, але неточним виразом, якщо стовпець позначений ПЕРСИСТИЧНО в операторі CREATE TABLE або ALTER TABLE. Це означає, що Database Engine зберігає обчислені значення в таблиці та оновлює їх, коли оновлюються будь-які інші стовпці, від яких залежить обчислений стовпець. База даних двигунів використовує ці збережені значення, коли створює індекс у стовпці та коли на індекс посилається запит. Цей параметр дозволяє створити індекс на обчисленому стовпчику, коли Database Engine не може з точністю довести, чи функція, яка повертає обчислювані вирази стовпців, зокрема функція CLR, створена в .NET Framework, є детермінованою і точною.

Отже, як кажуть документи "Database Engine зберігає обчислювані значення в таблиці" , а значення також зберігається в моєму індексі, чому для пошуку A, B і C потрібен пошук ключа, коли вони не посилаються на запит взагалі? Я припускаю, що вони використовуються для обчислення Comp, але чому? Крім того, чому запит може використовувати індекс на t2, а не на t1?

Запити та DDL у SQL Fiddle

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

Відповіді:


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обмеженні.


2
+1 BTW Я натрапив на інший випадок зайвого пошуку, дивлячись на це, що може (або не може) виявити інтерес. SQL Fiddle .
Мартін Сміт

@MartinSmith Так, це цікаво. Ще одне загальне правило переписати ( 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;

Це дало хороший план низької вартості, використовуючи ваш індекс і зі значно меншими показаннями, ніж інші параметри (і ті ж результати для ваших тестових даних).

Витрати плану 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'

1
Ну, він використовує індекс, але лише до точки. Якщо compце не обчислений стовпець, ви не бачите сортування.
Мартін Сміт

Спасибі. Мій фактичний сценарій не набагато складніший, і LEADфункція працювала саме так, як хотілося б у моєму локальному екземплярі 2012 express. На жаль, ця незначна незручність для мене ще не вважалася достатньою підставою для оновлення виробничих серверів ...
GarethD

-1

Коли я намагався виконувати ті самі дії, отримав ще один результат. По-перше, мій план виконання таблиці без індексів виглядає наступним чином:введіть тут опис зображення

Як ми бачимо з кластерного сканування індексів (t2), предикат використовується для визначення необхідних рядків, які потрібно повернути (через умову):

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

Коли індекс був доданий, неважливо, був він визначений оператором WITH чи ні, план виконання став таким:

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

Як ми бачимо, кластерне індексне сканування замінено на індексне сканування. Як ми бачили вище, SQL Server використовує вихідні стовпці обчисленої колонки для виконання відповідності вкладеного запиту. Під час кластерного сканування індексу всі ці значення можна отримати одночасно (додаткові операції не потрібні). Коли додано індекс, фільтрація необхідних рядків із таблиці (у головному виділенні) виконується відповідно до індексу, але значення стовпців-джерел для обчисленого стовпця compвсе-таки потрібно отримати (остання операція Nested Loop) .

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

Через це використовується операція пошуку ключів - для отримання даних вихідних стовпців обчисленої.

PS Виглядає як помилка в SQL Server.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.