Отримання сканування, хоча я очікую пошуку


9

Мені потрібно оптимізувати SELECTоператор, але SQL Server завжди виконує сканування індексу замість пошуку. Це запит, який, звичайно, є в збереженій процедурі:

CREATE PROCEDURE dbo.something
  @Status INT = NULL,
  @IsUserGotAnActiveDirectoryUser BIT = NULL    
AS

    SELECT [IdNumber], [Code], [Status], [Sex], 
           [FirstName], [LastName], [Profession], 
           [BirthDate], [HireDate], [ActiveDirectoryUser]
    FROM Employee
    WHERE (@Status IS NULL OR [Status] = @Status)
    AND 
    (
      @IsUserGotAnActiveDirectoryUser IS NULL 
      OR 
      (
        @IsUserGotAnActiveDirectoryUser IS NOT NULL AND       
        (
          @IsUserGotAnActiveDirectoryUser = 1 AND ActiveDirectoryUser <> ''
        )
        OR
        (
          @IsUserGotAnActiveDirectoryUser = 0 AND ActiveDirectoryUser = ''
        )
      )
    )

А це індекс:

CREATE INDEX not_relevent ON dbo.Employee
(
    [Status] DESC,
    [ActiveDirectoryUser] ASC
)
INCLUDE (...all the other columns in the table...); 

План:

План малюнка

Чому SQL Server обрав сканування? Як я можу це виправити?

Визначення стовпців:

[Status] int NOT NULL
[ActiveDirectoryUser] VARCHAR(50) NOT NULL

Параметри стану можуть бути:

NULL: all status,
1: Status= 1 (Active employees)
2: Status = 2 (Inactive employees)

IsUserGotAnActiveDirectoryUser може бути:

NULL: All employees
0: ActiveDirectoryUser is empty for that employee
1: ActiveDirectoryUser  got a valid value (not null and not empty)

Чи можете ви десь розмістити фактичний план виконання (не його зображення, а файл .sqlplan у формі XML)? Думаю, ви змінили процедуру, але насправді не отримали нової компіляції на рівні заяви. Чи можете ви змінити текст тексту запиту (наприклад, додати префікс схеми до імені таблиці ), а потім передати дійсне значення для @Status?
Аарон Бертран

1
Також визначення індексу задає питання - чому ключ увімкнено Status DESC? Скільки існує значень, для Statusчого вони (якщо число невелике) і чи кожне значення представлено приблизно однаково? Покажіть нам результатSELECT TOP (20) [Status], c = COUNT(*) FROM dbo.Employee GROUP BY [Status] ORDER BY c DESC;
Аарон Бертран

Відповіді:


11

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

Я називаю це процедурою «кухонної мийки» , оскільки ви очікуєте, що один запит надасть усі речі, включаючи кухонну мийку.

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

  • Будуйте оператор динамічно - це дозволить вам залишити зауваження із згадуванням стовпців, для яких не було надано жодних параметрів, і гарантує, що у вас буде план, оптимізований саме під фактичні параметри, передані зі значеннями.
  • ВикористанняOPTION (RECOMPILE) - це не дозволяє певним параметрам примусово застосовувати неправильний тип плану, особливо корисно, коли у вас є перекос даних, погана статистика або коли при першому виконанні оператора використовується нетипове значення, яке призведе до іншого плану, ніж пізніше і частіше страти.
  • Використовуйте параметр сервераoptimize for ad hoc workloads - це запобігає забрудненню кешу плану варіантами запитів, які використовуються лише один раз.

Увімкнути оптимізацію для спеціальних навантажень:

EXEC sys.sp_configure 'show advanced options', 1;
GO
RECONFIGURE WITH OVERRIDE;
GO
EXEC sys.sp_configure 'optimize for ad hoc workloads', 1;
GO
RECONFIGURE WITH OVERRIDE;
GO
EXEC sys.sp_configure 'show advanced options', 0;
GO
RECONFIGURE WITH OVERRIDE;

Змініть процедуру:

ALTER PROCEDURE dbo.Whatever
  @Status INT = NULL,
  @IsUserGotAnActiveDirectoryUser BIT = NULL
AS
BEGIN 
  SET NOCOUNT ON;
  DECLARE @sql NVARCHAR(MAX) = N'SELECT [IdNumber], [Code], [Status], 
     [Sex], [FirstName], [LastName], [Profession],
     [BirthDate], [HireDate], [ActiveDirectoryUser]
   FROM dbo.Employee -- please, ALWAYS schema prefix
   WHERE 1 = 1';

   IF @Status IS NOT NULL
     SET @sql += N' AND ([Status]=@Status)'

   IF @IsUserGotAnActiveDirectoryUser = 1
     SET @sql += N' AND ActiveDirectoryUser <> ''''';
   IF @IsUserGotAnActiveDirectoryUser = 0
     SET @sql += N' AND ActiveDirectoryUser = ''''';

   SET @sql += N' OPTION (RECOMPILE);';

   EXEC sys.sp_executesql @sql, N'@Status INT, @Status;
END
GO

Коли у вас є навантаження на основі того набору запитів, який ви можете відстежувати, ви можете проаналізувати виконання та побачити, які з них отримали б найбільшу користь із додаткових чи різних індексів - це можна зробити з різних ракурсів, з простого "яке поєднання параметри надаються найчастіше? " до "які окремі запити мають найдовший термін виконання?" Ми не можемо відповісти на ці запитання лише на основі вашого коду. Ми можемо лише припустити, що будь-який індекс буде корисним лише для підмножини всіх можливих комбінацій параметрів, які ви намагаєтесь підтримати. Наприклад, якщо@Statusє NULL, тоді не можна шукати проти цього некластеризованого індексу. Тож для тих випадків, коли користувачів не хвилює стан, ви збираєтеся перевірити, якщо ви не маєте індекс, який задовольняє інші пункти (але такий індекс теж не буде корисним, враховуючи поточну логіку запитів - або порожній рядок, або не порожній рядок не є абсолютно вибірковим).

У цьому випадку, залежно від набору можливих Statusзначень та того, наскільки розподілені ці значення, OPTION (RECOMPILE)можливо, це не знадобиться. Але якщо у вас є деякі значення, які дадуть 100 рядків і деякі значення, які дадуть сотні тисяч, ви можете захотіти його там (навіть за ціною процесора, яка повинна бути граничною, враховуючи складність цього запиту), щоб ви могли отримуйте пошуки в якомога більшій кількості випадків. Якщо діапазон значень достатньо обмежений, ви навіть можете зробити щось складне з динамічним SQL, де ви говорите: "Я маю це дуже селективне значення для @Status, тому коли це конкретне значення буде передано, зробіть цю незначну зміну тексту запиту, щоб це вважається іншим запитом і оптимізовано для цього значення парам ".


3
Я багато разів використовував цей підхід, і це фантастичний спосіб змусити оптимізатора робити все так, як ви думаєте, як це робити. Кім Тріпп розповідає про подібне рішення тут: sqlskills.com/blogs/kimberly/high-performance-procedures. У відео є сеанс, який вона зробила в PASS пару років тому, яка справді перетворюється на шалені деталі щодо того, чому це працює. Це означає, що це дійсно не додає тонни до того, що тут сказав пан Бертран. Це один із тих інструментів, які кожен повинен тримати у своєму інструментальному ремені. Це дійсно може врятувати величезні болі для цих запитів.
mskinner

3

Відмова від відповідальності : Деякі речі в цій відповіді можуть змусити DBA здригнутися. Я підходжу до цього з точки зору чистої продуктивності - як отримати пошукові запити, коли ви завжди отримуєте індекси сканування.

Із цим не виходить, тут іде.

Ваш запит - це те, що відомо як "запит кухонної раковини" - один запит, який повинен забезпечити широкий спектр можливих умов пошуку. Якщо користувач встановив @statusзначення, потрібно фільтрувати цей статус. Якщо @statusє NULL, поверніть усі статуси тощо.

Це спричиняє проблеми з індексуванням, але вони не пов'язані із зручністю пошуку, оскільки всі ваші умови пошуку відповідають критеріям.

Це можна стверджувати:

WHERE [status]=@status

Це не піддається обробці, оскільки SQL Server повинен оцінювати ISNULL([status], 0)кожен рядок, а не шукати одне значення в індексі:

WHERE ISNULL([status], 0)=@status

Я відтворив кухонну мийку в більш простому вигляді:

CREATE TABLE #work (
    A    int NOT NULL,
    B    int NOT NULL
);

CREATE UNIQUE INDEX #work_ix1 ON #work (A, B);

INSERT INTO #work (A, B)
VALUES (1,  1), (2,  1),
       (3,  1), (4,  1),
       (5,  2), (6,  2),
       (7,  2), (8,  3),
       (9,  3), (10, 3);

Якщо ви спробуєте наступне, ви отримаєте індексне сканування, навіть якщо A є першим стовпцем індексу:

DECLARE @a int=4, @b int=NULL;

SELECT *
FROM #work
WHERE (@a IS NULL OR @a=A) AND
      (@b IS NULL OR @b=B);

Це, однак, призводить до пошуку індексу:

DECLARE @a int=4, @b int=NULL;

SELECT *
FROM #work
WHERE @a=A AND
      @b IS NULL;

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

DECLARE @a int=4, @b int=NULL;

SELECT *
FROM #work
WHERE @a=A AND
      @b IS NULL
UNION ALL
SELECT *
FROM #work
WHERE @a=A AND
      @b=B
UNION ALL
SELECT *
FROM #work
WHERE @a IS NULL AND
      @b=B
UNION ALL
SELECT *
FROM #work
WHERE @a IS NULL AND
      @b IS NULL;

Для того, щоб третій із цих чотирьох використав Index Search, вам знадобиться другий індекс на (B, A). Ось як може виглядати ваш запит із цими змінами (включаючи рефакторинг запиту, щоб зробити його більш читабельним).

DECLARE @Status int = NULL,
        @IsUserGotAnActiveDirectoryUser bit = NULL;

SELECT [IdNumber], [Code], [Status], [Sex], [FirstName], [LastName],
       [Profession], [BirthDate], [HireDate], [ActiveDirectoryUser]
FROM Employee
WHERE [Status]=@Status AND
      @IsUserGotAnActiveDirectoryUser IS NULL

UNION ALL

SELECT [IdNumber], [Code], [Status], [Sex], [FirstName], [LastName],
       [Profession], [BirthDate], [HireDate], [ActiveDirectoryUser]
FROM Employee
WHERE [Status]=@Status AND
      @IsUserGotAnActiveDirectoryUser=1 AND ActiveDirectoryUser<>''

UNION ALL

SELECT [IdNumber], [Code], [Status], [Sex], [FirstName], [LastName],
       [Profession], [BirthDate], [HireDate], [ActiveDirectoryUser]
FROM Employee
WHERE [Status]=@Status AND
      @IsUserGotAnActiveDirectoryUser=0 AND (ActiveDirectoryUser IS NULL OR ActiveDirectoryUser='')

UNION ALL

SELECT [IdNumber], [Code], [Status], [Sex], [FirstName], [LastName],
       [Profession], [BirthDate], [HireDate], [ActiveDirectoryUser]
FROM Employee
WHERE @Status IS NULL AND
      @IsUserGotAnActiveDirectoryUser IS NULL

UNION ALL

SELECT [IdNumber], [Code], [Status], [Sex], [FirstName], [LastName],
       [Profession], [BirthDate], [HireDate], [ActiveDirectoryUser]
FROM Employee
WHERE @Status IS NULL AND
      @IsUserGotAnActiveDirectoryUser=1 AND ActiveDirectoryUser<>''

UNION ALL

SELECT [IdNumber], [Code], [Status], [Sex], [FirstName], [LastName],
       [Profession], [BirthDate], [HireDate], [ActiveDirectoryUser]
FROM Employee
WHERE @Status IS NULL AND
      @IsUserGotAnActiveDirectoryUser=0 AND (ActiveDirectoryUser IS NULL OR ActiveDirectoryUser='');

... плюс вам знадобиться додатковий індекс на Employeeдва стовпці індексу.

Для повноти я повинен зазначити, що x=@xнеявно означає, що xне може бути, NULLтому що NULLніколи не дорівнює NULL. Це трохи спрощує запит.

І так, динамічна відповідь SQL Аарона Бертран є кращим вибором у більшості випадків (тобто, коли можна жити з перекомпіляціями).


3

Ваше основне питання, здається, "Чому", і я думаю, що ви можете знайти відповідь приблизно за 55 хвилин цієї великої презентації Адама Маханіка в TechEd кілька років тому.

Я згадую 5 хвилин у 55 хвилинах, але вся презентація варта часу. Якщо ви подивитесь на план запиту свого запиту, я впевнений, що ви знайдете, що в ньому є Залишкові предикати для пошуку. В основному SQL не може "бачити" всі частини індексу, оскільки деякі з них приховані нерівностями та іншими умовами. Результатом є сканування індексу для супернабору на основі предиката. Цей результат скручується та повторно сканується за допомогою залишкового предиката.

Перевірте властивості Оператора сканування (F4) і перевірте, чи є у списку властивостей "Шукати предикат" і "Передказ".

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


0

Перш ніж ми ставимо питання про те, чи віддається перевага індексу над скануванням індексу, одним із головних правил є перевірка кількості повернених рядків проти загальних рядків нижньої таблиці. Наприклад, якщо ви очікуєте, що ваш запит поверне 10 рядків з 1 мільйона рядків, то пошук пошуку в індексах, ймовірно, є більш кращим, ніж сканування індексів. Однак якщо з запиту потрібно повернути кілька тисяч рядків (або більше), то пошук НЕ МОЖЕТ бути бажаним.

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


Відфільтрувавши кілька тисяч рядків з таблиці в 1 мільйон, я все одно хотів би шукати - це все ще величезне поліпшення продуктивності від сканування всієї таблиці.
Даніель Хатмахер

-6

це лише оригінальний формат

DECLARE @Status INT = NULL,
        @IsUserGotAnActiveDirectoryUser BIT = NULL    

SELECT [IdNumber], [Code], [Status], [Sex], [FirstName], [LastName], [Profession],
       [BirthDate], [HireDate], [ActiveDirectoryUser]
FROM Employee
WHERE (@Status IS NULL OR [Status]=@Status)  
AND (            @IsUserGotAnActiveDirectoryUser IS NULL 
      OR (       @IsUserGotAnActiveDirectoryUser IS NOT NULL 
           AND (     @IsUserGotAnActiveDirectoryUser = 1 
                 AND ActiveDirectoryUser <> '') 
           OR  (     @IsUserGotAnActiveDirectoryUser = 0 
                 AND ActiveDirectoryUser =  '')
         )
    )

це перегляд - не на 100% впевнений у цьому, але (можливо), спробуйте
навіть один АБО, ймовірно, це буде проблемою,
це зламається на ActiveDirectoryUser null

  WHERE isnull(@Status, [Status]) = [Status]
    AND (      (     isnull(@IsUserGotAnActiveDirectoryUser, 1) = 1 
                 AND ActiveDirectoryUser <> '' ) 
           OR  (     isnull(@IsUserGotAnActiveDirectoryUser, 0) = 0 
                 AND ActiveDirectoryUser =  '' )
        )

3
Мені незрозуміло, як ця відповідь вирішує питання ОП.
Ерік

@Erik Чи може нам подобатися, можливо, ОП спробує? Два АБО пішли. Ви точно знаєте, що це не може допомогти виконати запити?
папараццо

@ ypercubeᵀᴹ IsUserGotAnActiveDirectoryUser НЕ NULL видалено. Ці два непотрібних видалити АБО та видалити IsUserGotAnActiveDirectoryUser NULL. Ви впевнені, що цей запит не працюватиме швидко, ніж OP?
папараццо

@ ypercubeᵀᴹ Могла б зробити багато справ. Я не шукаю більш простого. Два або пішли. Або, як правило, погано для запитів. Я дістаюсь тут є такий клуб, і я не є членом клубу. Але я цим займаюся на життя і розміщую те, що я знаю, що працювало. На мої відповіді не впливають голоси.
папараццо
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.