Почему этот запрос не использует мой некластеризованный индекс и как я могу его выполнить?


12

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

Этот запрос выполняется примерно за 2,5 секунды:

SELECT TOP 1000 * FROM [CIA_WIZ].[dbo].[Heartbeats]
WHERE [DateEntered] BETWEEN '2011-08-30' and '2011-08-31';

Этот работает около 33 мс:

SELECT TOP 1000 * FROM [CIA_WIZ].[dbo].[Heartbeats]
WHERE [DateEntered] BETWEEN '2011-08-30' and '2011-08-31' 
ORDER BY [DateEntered], [DeviceID];

В поле [ID] (pk) есть кластеризованный индекс, а в [DateEntered], [DeviceID] есть некластеризованный индекс. Первый запрос использует кластеризованный индекс, второй запрос использует мой некластеризованный индекс. Мой вопрос состоит из двух частей:

  • Почему, поскольку оба запроса имеют предложение WHERE в поле [DateEntered], сервер использует кластеризованный индекс для первого, а не для второго?
  • Как сделать так, чтобы некластерный индекс по умолчанию использовался в этом запросе даже без orderby? (Или почему бы мне не хотеть такого поведения?)

DateEntered - это DateTime, в данном случае я использую часть даты, но иногда я запрашиваю одновременно дату и время.
конец

Ответы:


9

В первом запросе выполняется сканирование таблицы на основе порога, который я ранее объяснил: возможно ли повысить производительность запроса в узкой таблице с миллионами строк?

(Скорее всего, ваш запрос без TOP 1000предложения вернет более 46 тыс. строк или около 35 тыс. - 46 тыс. (серая область ;-)).

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

измените порядок столбцов в ORDER BYпредложении, и вы вернетесь к сканированию кластеризованного индекса, поскольку NC INDEX тогда бесполезен.

изменить забыл ответ на ваш второй вопрос, почему вы не хотите этого

Использование некластеризованного непокрывающего индекса означает, что в индексе NC ищется rowID, а затем необходимо искать пропущенные столбцы в кластеризованном индексе (кластеризованный индекс содержит все столбцы таблицы). IO для поиска отсутствующих столбцов в кластерном индексе - это Random IO.

Ключ здесь СЛУЧАЙНЫЙ. потому что для каждой строки, найденной в индексе NC, методы доступа должны искать новую страницу в кластерном индексе. Это случайно, а потому очень дорого.

Теперь, с другой стороны, оптимизатор также может пойти на сканирование кластерного индекса. Он может использовать карты распределения для поиска диапазонов сканирования и просто начать чтение кластеризованного индекса большими кусками. Это последовательно и намного дешевле. (пока ваша таблица не фрагментирована :-)) Недостатком является то, что ВСЕ кластерный индекс необходимо прочитать. Это плохо для вашего буфера и потенциально огромного количества операций ввода-вывода. но все же, последовательные операции ввода-вывода.

В вашем случае оптимизатор выбирает где-то между 35k и 46k строк, это дешевле для полного сканирования кластерного индекса. Да, это неправильно. И во многих случаях с узкими некластеризованными индексами, не относящимися к выборочным WHEREпредложениям или большими таблицами в этом отношении, это идет не так. (Ваш стол хуже, потому что это также очень узкий стол.)

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

Таким образом, ваш заказ является идеальным решением для «подсказки запроса». НО, в определенный момент, когда результаты вашего запроса станут такими большими, штраф за случайные операции ввода-вывода в закладках будет настолько большим, что станет медленнее. Я предполагаю, что оптимизатор изменит планы обратно на сканирование кластерного индекса до этого момента, но вы никогда не узнаете наверняка.

В вашем случае, если ваши вставки упорядочены по enterdate, как обсуждалось в чате и в предыдущем вопросе (см. Ссылку), вам лучше создавать кластеризованный индекс для столбца enterDate.


20

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

SELECT
    [ID],
    [DeviceID],
    [IsPUp],
    [IsWebUp],
    [IsPingUp],
    [DateEntered]
FROM [dbo].[Heartbeats]
WHERE
    [ID] IN
(
    -- Keys
    SELECT TOP (1000)
        [ID]
    FROM [dbo].[Heartbeats]
    WHERE 
        [DateEntered] >= CONVERT(datetime, '2011-08-30', 121)
        AND [DateEntered]  < CONVERT(datetime, '2011-08-31', 121)
);

План запроса

Сравните этот план с планом, созданным, когда некластеризованный индекс форсируется с подсказкой:

SELECT TOP (1000) 
    * 
FROM [dbo].[Heartbeats] WITH (INDEX(CommonQueryIndex))
WHERE 
    [DateEntered] BETWEEN '2011-08-30' and '2011-08-31';

План подсказок по форсированным индексам

Планы по сути одинаковы (поиск ключей - не что иное, как поиск по кластерному индексу). Обе формы плана будут когда-либо выполнять только один поиск по некластеризованному индексу и максимум 1000 поисков в кластеризованном индексе.

Важным отличием является положение оператора Top. Расположенный между двумя поисками, Top не позволяет оптимизатору заменить две операции поиска логически эквивалентным сканированием кластеризованного индекса. Оптимизатор работает, заменяя части логического плана эквивалентными реляционными операциями. Top не является реляционным оператором, поэтому перезапись предотвращает преобразование в просмотр кластеризованного индекса. Если бы оптимизатор мог изменить положение оператора Top, он все равно предпочел бы сканирование, а не поиск + поиск из-за способа оценки затрат.

Калькуляция сканирования и поиска

На очень высоком уровне модель затрат оптимизатора для сканирования и поиска довольно проста: она оценивает, что 320 случайных операций поиска стоят столько же, сколько считывание 1350 страниц при сканировании. Это, вероятно, мало похоже на аппаратные возможности какой-либо конкретной современной системы ввода / вывода, но работает достаточно хорошо в качестве практической модели.

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

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

Все это (и даже больше) означает, что оптимизатор имеет тенденцию переключаться на сканирование раньше, чем, вероятно, следует. Случайный ввод-вывод только «намного дороже», чем «последовательный» ввод-вывод, если возникает физическая операция - доступ к страницам в памяти действительно очень быстрый. Даже там, где требуется физическое чтение, сканирование может вообще не приводить к последовательному чтению из-за фрагментации, и поиски могут быть расположены так, что шаблон по существу является последовательным. Добавьте к этому изменяющуюся характеристику производительности современных систем ввода / вывода (особенно твердотельных), и все это начинает выглядеть очень шатко.

Ряд голов

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

Изображения в этом ответе были созданы с помощью SQL Sentry Plan Explorer .

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.