Чому сканування швидше, ніж шукати цей предикат?


30

Мені вдалося відтворити питання щодо виконання запитів, який я б назвав несподіваним. Я шукаю відповідь, зосереджена на внутрішніх справах.

На моїй машині наступний запит виконує кластерне сканування індексу і займає приблизно 6,8 секунди часу процесора:

SELECT ID1, ID2
FROM two_col_key_test WITH (FORCESCAN)
WHERE ID1 NOT IN
(
N'1', N'2',N'3', N'4', N'5',
N'6', N'7', N'8', N'9', N'10',
N'11', N'12',N'13', N'14', N'15',
N'16', N'17', N'18', N'19', N'20'
)
AND (ID1 = N'FILLER TEXT' AND ID2 >= N'' OR (ID1 > N'FILLER TEXT'))
ORDER BY ID1, ID2 OFFSET 12000000 ROWS FETCH FIRST 1 ROW ONLY
OPTION (MAXDOP 1);

Наступний запит шукає кластерний індекс (лише різниця - це видалення FORCESCANпідказки), але займає близько 18,2 секунди часу процесора:

SELECT ID1, ID2
FROM two_col_key_test
WHERE ID1 NOT IN
(
N'1', N'2',N'3', N'4', N'5',
N'6', N'7', N'8', N'9', N'10',
N'11', N'12',N'13', N'14', N'15',
N'16', N'17', N'18', N'19', N'20'
)
AND (ID1 = N'FILLER TEXT' AND ID2 >= N'' OR (ID1 > N'FILLER TEXT'))
ORDER BY ID1, ID2 OFFSET 12000000 ROWS FETCH FIRST 1 ROW ONLY
OPTION (MAXDOP 1);

Плани запитів досить схожі. Для обох запитів 120000001 рядків, прочитаних з кластерного індексу:

плани запитів

Я на SQL Server 2017 CU 10. Ось код для створення та заповнення two_col_key_testтаблиці:

drop table if exists dbo.two_col_key_test;

CREATE TABLE dbo.two_col_key_test (
    ID1 NVARCHAR(50) NOT NULL,
    ID2 NVARCHAR(50) NOT NULL,
    FILLER NVARCHAR(50),
    PRIMARY KEY (ID1, ID2)
);

DROP TABLE IF EXISTS #t;

SELECT TOP (4000) 0 ID INTO #t
FROM master..spt_values t1
CROSS JOIN master..spt_values t2
OPTION (MAXDOP 1);


INSERT INTO dbo.two_col_key_test WITH (TABLOCK)
SELECT N'FILLER TEXT' + CASE WHEN ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) > 8000000 THEN N' 2' ELSE N'' END
, ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
, NULL
FROM #t t1
CROSS JOIN #t t2;

Я сподіваюся на відповідь, яка не більше, ніж звітування про стек дзвінків. Наприклад, я бачу, що sqlmin!TCValSSInRowExprFilter<231,0,0>::GetDataXу повільному запиті потрібно значно більше циклів процесора порівняно з швидким:

перегляд

Замість того, щоб зупинятися на цьому, я хотів би зрозуміти, що це таке, і чому існує така велика різниця між двома запитами.

Чому для цих двох запитів велика різниця у процесі процесора?

Відповіді:


31

Чому для цих двох запитів велика різниця у процесі процесора?

План сканування оцінює наступний висунутий немаргійний (залишковий) предикат для кожного рядка:

[two_col_key_test].[ID1]<>N'1' 
AND [two_col_key_test].[ID1]<>N'10' 
AND [two_col_key_test].[ID1]<>N'11' 
AND [two_col_key_test].[ID1]<>N'12' 
AND [two_col_key_test].[ID1]<>N'13' 
AND [two_col_key_test].[ID1]<>N'14' 
AND [two_col_key_test].[ID1]<>N'15' 
AND [two_col_key_test].[ID1]<>N'16' 
AND [two_col_key_test].[ID1]<>N'17' 
AND [two_col_key_test].[ID1]<>N'18' 
AND [two_col_key_test].[ID1]<>N'19' 
AND [two_col_key_test].[ID1]<>N'2' 
AND [two_col_key_test].[ID1]<>N'20' 
AND [two_col_key_test].[ID1]<>N'3' 
AND [two_col_key_test].[ID1]<>N'4' 
AND [two_col_key_test].[ID1]<>N'5' 
AND [two_col_key_test].[ID1]<>N'6' 
AND [two_col_key_test].[ID1]<>N'7' 
AND [two_col_key_test].[ID1]<>N'8' 
AND [two_col_key_test].[ID1]<>N'9' 
AND 
(
    [two_col_key_test].[ID1]=N'FILLER TEXT' 
    AND [two_col_key_test].[ID2]>=N'' 
    OR [two_col_key_test].[ID1]>N'FILLER TEXT'
)

сканування залишкове

План пошуку робить дві пошукові операції:

Seek Keys[1]: 
    Prefix: 
    [two_col_key_test].ID1 = Scalar Operator(N'FILLER TEXT'), 
        Start: [two_col_key_test].ID2 >= Scalar Operator(N'')
Seek Keys[1]: 
    Start: [two_col_key_test].ID1 > Scalar Operator(N'FILLER TEXT')

... відповідати цій частині присудка:

(ID1 = N'FILLER TEXT' AND ID2 >= N'' OR (ID1 > N'FILLER TEXT'))

Залишковий присудок застосовується до рядків, які передають умови пошуку вище (усі рядки у вашому прикладі).

Однак кожна нерівність замінюється двома окремими тестами на менше, OR ніж :

([two_col_key_test].[ID1]<N'1' OR [two_col_key_test].[ID1]>N'1') 
AND ([two_col_key_test].[ID1]<N'10' OR [two_col_key_test].[ID1]>N'10') 
AND ([two_col_key_test].[ID1]<N'11' OR [two_col_key_test].[ID1]>N'11') 
AND ([two_col_key_test].[ID1]<N'12' OR [two_col_key_test].[ID1]>N'12') 
AND ([two_col_key_test].[ID1]<N'13' OR [two_col_key_test].[ID1]>N'13') 
AND ([two_col_key_test].[ID1]<N'14' OR [two_col_key_test].[ID1]>N'14') 
AND ([two_col_key_test].[ID1]<N'15' OR [two_col_key_test].[ID1]>N'15') 
AND ([two_col_key_test].[ID1]<N'16' OR [two_col_key_test].[ID1]>N'16') 
AND ([two_col_key_test].[ID1]<N'17' OR [two_col_key_test].[ID1]>N'17') 
AND ([two_col_key_test].[ID1]<N'18' OR [two_col_key_test].[ID1]>N'18') 
AND ([two_col_key_test].[ID1]<N'19' OR [two_col_key_test].[ID1]>N'19') 
AND ([two_col_key_test].[ID1]<N'2' OR [two_col_key_test].[ID1]>N'2') 
AND ([two_col_key_test].[ID1]<N'20' OR [two_col_key_test].[ID1]>N'20') 
AND ([two_col_key_test].[ID1]<N'3' OR [two_col_key_test].[ID1]>N'3') 
AND ([two_col_key_test].[ID1]<N'4' OR [two_col_key_test].[ID1]>N'4') 
AND ([two_col_key_test].[ID1]<N'5' OR [two_col_key_test].[ID1]>N'5') 
AND ([two_col_key_test].[ID1]<N'6' OR [two_col_key_test].[ID1]>N'6') 
AND ([two_col_key_test].[ID1]<N'7' OR [two_col_key_test].[ID1]>N'7') 
AND ([two_col_key_test].[ID1]<N'8' OR [two_col_key_test].[ID1]>N'8') 
AND ([two_col_key_test].[ID1]<N'9' OR [two_col_key_test].[ID1]>N'9')

шукати залишковий

Переписування кожної нерівності, наприклад:

[ID1] <> N'1'  ->  [ID1]<N'1' OR [ID1]>N'1'

... тут контрпродуктивний. Порівняння струнних порівнянь є дорогим. Подвоєння кількості порівнянь пояснює більшу частину різниці в процесі процесора, який ви бачите.

Це можна зрозуміти більш чітко, відключивши натискання неагрегованих предикатів з недокументованим прапором сліду 9130. Це відображатиметься залишковий як окремий фільтр, інформацію про продуктивність ви можете перевірити окремо:

сканування

шукати

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

Хоча перезапис нерівності може зробити можливим (можливо, відфільтрованим) відповідність індексів (щоб найкраще використовувати шукаючу здатність індексів b-дерева), було б краще згодом відновити це розширення, якщо обидві половини опиняться в залишку. Ви можете запропонувати це як покращення на сайті зворотного зв'язку SQL Server .

Зауважимо також, що оригінальна ("застаріла") модель оцінки кардинальності трапляється для вибору сканування за замовчуванням для цього запиту.

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