Получение сканирования, хотя я ожидаю поиска

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)
Bestter
источник
Можете ли вы опубликовать фактический план выполнения где-нибудь (не его изображение, а файл .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

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

В этом случае, в зависимости от набора возможных Statusзначений и от того, как распределены эти значения, OPTION (RECOMPILE)может не потребоваться. Но если у вас есть некоторые значения, которые приведут к 100 строкам, и некоторые значения, которые приведут к сотням тысяч, вы можете захотеть это там (даже при стоимости процессора, которая должна быть незначительной, учитывая сложность этого запроса), так что вы можете получить ищет в максимально возможном количестве случаев. Если диапазон значений достаточно ограничен, вы можете даже сделать что-то хитрое с динамическим SQL, где вы скажете: «У меня есть это очень избирательное значение @Status, поэтому, когда это конкретное значение передается, внесите это небольшое изменение в текст запроса, чтобы это считается другим запросом и оптимизировано для этого значения параметра. "

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

Отказ от ответственности : некоторые вещи в этом ответе могут заставить ДБА вздрогнуть. Я подхожу к этому с чистой точки зрения производительности - как получить индексный поиск, когда вы всегда получаете индексное сканирование.

С этим из пути, здесь идет.

Ваш запрос - это так называемый «запрос кухонной раковины» - один запрос, предназначенный для удовлетворения целого ряда возможных условий поиска. Если пользователь устанавливает @statusзначение, вы хотите фильтровать это состояние. Если @statusесть NULL, вернуть все статусы и так далее.

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

Это саркастично

WHERE [status]=@status

Это не sargable , поскольку 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;

Однако для того, чтобы третий из этих четырех использовал поиск по индексу, вам понадобится второй индекс (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 миллиона строк, тогда поиск по индексу, вероятно, будет более предпочтительным, чем сканирование по индексу. Однако, если несколько тысяч строк (или более) должны быть возвращены из запроса, тогда поиск по индексу НЕ обязательно может быть предпочтительным.

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

jyao
источник
Отфильтровывая несколько тысяч строк из таблицы в 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
Мне неясно, как этот ответ решает вопрос ОП.
Эрик
@ Эрик Можем ли мы, возможно, позволить ОП попробовать? Два ИЛИ ушли. Знаете ли вы наверняка, что это не может помочь производительности запросов?
папарацци
@ ypercubeᵀᴹ IsUserGotAnActiveDirectoryUser IS NOT NULL удален. Эти два ненужных удалить ИЛИ и удалить IsUserGotAnActiveDirectoryUser IS NULL. Вы уверены, что этот запрос не будет работать быстрее, чем OP?
Папараццо
@ ypercubeᵀᴹ Мог бы сделать много вещей. Я не ищу проще. Двое или ушли. Или, как правило, плохо для планов запросов. У меня тут какой-то клуб, и я не являюсь его частью. Но я делаю это для жизни и публикую то, что, как я знаю, сработало. На мои ответы не влияют отрицательные голоса.
Папараццо