Skip to content

Порядок столбцов в индексах

Пересказ статьи Mike Byrd. Index Column Order – Be Happy!


Мне всегда было интересно, как SQL Server выбирает порядок столбцов в предлагаемых индексах. И недавно я просто не смог сопротивляться искушению проверить это. Рассмотрим запрос:

Базовый запрос


Здесь используется база данных AdventureWorks2012Big, которая была получена из базы AdventureWorks2012 с помощью скрипта Jonathan Kehayias, доступного по адресу. Этот скрипт добавляет две новые таблицы (Sales.SalesOrderHeaderBig и Sales.SalesOrderDetailBig) и увеличивает число строк в SalesOrderHeader с 31465 до 1290065. Фактически после исследования (когда я закончил черновой вариант) я обнаружил, что в обеих таблицы, Sales.SalesOrderHeader и Sales.SalesOrderHeaderBig, столбец Status содержит только одно значение 5 во всех строках. Используя генератор случайных чисел, я модифицировал столбец Status таблицы Sales.SalesOrderHeaderBig таким образом, чтобы он содержал примерно равное число значений в диапазоне от 0 до 9. Это, вероятно, более соответствует реальным данным. Вы можете найти соответствующий код в приложенном файле.

Этот запрос представляет собой типичный итоговый отчет, в котором в предложении WHERE используются столбцы Status и OrderDate. Если я выполню этот запрос с индексами, изначально имеющимися в adventureWorks2012Big, то, как видно из приведенного ниже плана запроса, получу сканирование кластеризованного индекса на adventureWorks2012Big, где кластеризованный индекс создан на первичном ключе SalesOrderID (столбец identity):


План запроса с оригинальными индексами


В результате имеем:

  • стоимость запроса = 22.9441

  • логические чтения = 30311

  • cpu = 173мс


Отметим, что предложенный некластеризованный индекс определяется на Status, OrderDate со столбцами SalesPersonID, Subtotal, включенными в предложение INCLUDE. Это заставило меня задуматься о том, почему OrderDate не является первым. Все мы знаем (из общеприняных рекомендаций), что первым должен следовать наиболее селестивный столбец.

Итак, если я определю новый покрывающий индекс (как предлагается):

Microsoft предложил покрывающий индекс


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

И снова выполняя тот же запрос, мы получим следующие результаты:

План запроса с поиском в некластеризованном индексе на основе Status, OrderDate


Этот запрос дает:

  • стоимость запроса = 0.563195

  • логические чтения = 12

  • cpu = 0мс


Ух ты; улучшение производительности в 40 раз (по стоимости)! Покрывающий индекс дает нам теперь поиск в индексе. Но я все еще интересуюсь порядком столбцов в определении индекса. Поэтому давайте удалим новый индекс и определим другой на основе OrderDate, Status, как показано ниже:

Перестроенный некластеризованный индекс на основе OrderDate, Status


Опять выполним тот же запрос. Теперь имеем

План запроса с некластеризованным индексом на основе OrderDate, Status


Этот запрос дает:

  • стоимость запроса = 1.14707

  • логические чтения = 72

  • cpu = 15мс


Мы по-прежнему получаем поиск по индексу, но статистика по вводу/выводу и времени несколько ухудшилась, а стоимость плана запроса немного повысилась. Стоимость Index Seek подскочила с 31 до 57% от полной стоимости запроса. Почему так?

Если мы вернемся к плану запроса и переместим курсор мыши на оператор Index Seek в соответствующих планах первого, второго и третьего запросов, получим:

Свойства Index Seek в плане запроса для Index Scan и поисками в некластеризованном индексе (по порядку)


Можно получить интересные результаты из этих трех запросов. Главное отличие обнаруживается в первом запросе со сканированием кластерного индекса, в котором каждая строка таблицы проверяется на попадание в диапазон OrderDate и status = 5, что приводит к 30311 логических чтений (Predicate). Во втором запросе (рекомендуемый Майкрософт покрывающий некластеризованный индекс) вы имеете только предикат поиска (Seek Predicate), а во втором запросе со сканированием индекса у вас есть и Seek Predicate, и Predicate.

Так в чем же разница между Seek Predicate и Predicate в окне оператора? Seek Predicate сопровождается восстановлением данных, фильтрующим строки в пределах соответствующего индекса (действует почти как предложение WHERE). Predicate затем убирает строки в соответствии с заданным критерием. Поэтому, хотя тут имеется всего один оператор, Seek Predicate фильтрует строку во время чтения данных, после чего Predicate фильтрует результирующий набор по промежутку времени. Итак, в оригинальном сканировании кластеризованного индекса отсутствует Seek Predicate, поскольку требуется доступ к каждой строке. После чего Predicate фильтрует результирующий набор по промежутку времени на основании OrderDate и Status.

Во втором запросе, использующем предложенный покрывающий индекс, имеется только Seek Predicate. Оптимизатор принимает во внимание тот факт, что при заданном значении Status =5 OrderDate упорядочены по возрастанию в пределах индекса. Это не так в случае некластеризованного покрывающего индекса на основе OrderDate и Status соответственно. В этом случае имеем накрученное предложение "WHERE" на OrderDate и Status с последующей фильтрацией только по Status = 5. Вот почему результаты во втором запросе имеют только 12 логических чтений, в то время как третий запрос имеет 72 логических чтения из-за двойной фильтрации.

Итак, что случится, если я вернусь назад и перенесу OrderDate в предложение INCLUDE в IX_SalesOrderHeaderBig_Status, как это показано ниже (с очисткой некоторых индексов):

Перенос OrderDate в предложение INCLUDE


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

План запроса для некластеризованного индекса на базе Status


Этот запрос теперь имеет

  • стоимость запроса = 0.919195

  • логические чтения = 367

  • cpu = 31мс


Если поместить курсор над оператором Index seek, увидим

Свойства Index seek в плане выполнения для определения третьего индекса


Мы снова имеем разбиение на Predicate и Seek Predicate, но теперь Seek Predicate основывается только на столбце Status, а Predicate - на столбце OrderDate. В этом случае производительность лучше, чем оригинальное сканирование кластеризованного индекса, но не предложенного покрывающего индекса.

Часто меня спрашивают, какие столбцы включать в определение индекса. Я обычно отвечаю в духе Майкрософт: "Это зависит!", но в данных случаях это действительно зависит от данных, сценария и того, сможет ли оптимизатор воспользоваться упорядоченностью второго (и, возможно, третьего, если он есть) столбца.

Изначально общие результаты показались мне противоречащими интуиции, т.к. я думал, что OrderDate являлся более селективным столбцом. Но очевидно упорядочение OrderDate в индексе, предложенном оптимизатором привело к меньшему числу логических чтений, как было показано. В любом случае все три некластеризованных покрывающих индекса приводили к улучшению производительности (меньше логических чтений) по сравнению с оригинальным сканированием кластеризованного индекса. Но только то, что вы построили покрывающий индекс, не обязательно означает лучший вариант. Тестирование всегда должно предшествовать реализации.

Следует прокомментировать мой параметр DATA_COMPRESSION = ROW в определении трех индексов. Мой опыт в сжатии данных показал много преимуществ и почти никаких недостатков использования сжатия данных строк, поскольку данные сжимаются полностью с жесткого диска в буферный кэш, в кэш процессора L3 и, наконец, сжимаются/распаковываются в кэше процессора L2. Сжатие данных появилось в редакции SS2008 Enterprise Edition, а теперь в SS2016 SP2 Standard Edition и выше.

Обратные ссылки

Нет обратных ссылок

Комментарии

Показывать комментарии Как список | Древовидной структурой

Нет комментариев.

Автор не разрешил комментировать эту запись

Добавить комментарий

Enclosing asterisks marks text as bold (*word*), underscore are made via _word_.
Standard emoticons like :-) and ;-) are converted to images.

To prevent automated Bots from commentspamming, please enter the string you see in the image below in the appropriate input box. Your comment will only be submitted if the strings match. Please ensure that your browser supports and accepts cookies, or your comment cannot be verified correctly.
CAPTCHA

Form options

Добавленные комментарии должны будут пройти модерацию прежде, чем будут показаны.