План выполнения НЕ использует INDEX, он использует сканирование таблицы

9

Я знаю, что когда дело доходит до использования индекса или сканирования таблицы, SQL Server использует статистику, чтобы определить, какая из них лучше.

У меня есть таблица с 20 миллионами строк. У меня есть индекс (SnapshotKey, Measure) и этот запрос:

select Measure, SnapshotKey, MeasureBand
from t1
where Measure = 'FinanceFICOScore'
group by Measure, SnapshotKey, MeasureBand

Запрос возвращает 500 тыс. Строк. Таким образом, запрос выбирает только 2,5% строк таблицы.

Вопрос в том, почему SQL Server не использует некластеризованный индекс, а вместо этого использует сканирование таблицы?

Статистика обновляется.

Хорошо отметить, что производительность запросов хорошая.

Сканирование таблицы

Сканирование таблицы

Принудительный индекс

Индекс силы

Структура таблицы / индекса

CREATE TABLE [t1](
    [SnapshotKey] [int] NOT NULL,
    [SnapshotDt] [date] NOT NULL,
    [Measure] [nvarchar](30) NOT NULL,
    [MeasureBand] [nvarchar](30) NOT NULL,
    -- and many more fields
) ON [PRIMARY]

На столе нет PK, так как это хранилище данных.

CREATE NONCLUSTERED INDEX [nci_SnapshotKeyMeasure] ON [t1]
(
    [SnapshotKey] ASC,
    [Measure] ASC
)

источник

Ответы:

16

Поиск по индексу может быть не лучшим выбором, если вы возвращаете много строк и / или строки очень широки. Поиск может быть дорогим, если ваш индекс не покрывает. Смотрите № 2 здесь .

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

Оптимизатор всегда выбирает альтернативу с наименьшими затратами, которую он рассматривает. Если вы посмотрите на свойство Estimated Subtree Cost в корневом узле двух планов выполнения, то увидите, что план сканирования имеет более низкую оценочную стоимость, чем план поиска. В результате оптимизатор выбрал скан. По сути, это ответ на ваш вопрос.

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

Тем не менее, было показано , что модель затрат в целом дает в целом «достаточно хорошие» планы для большинства запросов, в большинстве схем баз данных, в большинстве конфигураций оборудования, в большинстве случаев, везде. Это настоящее достижение, если подумать.

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

Аарон Бертран
источник
9

На самом деле у вас есть 595,947 подходящих строк, что составляет около 3% ваших данных. Таким образом, стоимость поиска складывается быстро. Предположим, у вас есть 100 строк на страницу в вашей таблице, это 200 000 страниц для чтения при сканировании таблицы. Это намного дешевле, чем 595 947 просмотров.

С GROUP BYпредложением в вопросе, я думаю, вам будет лучше, если включен составной ключ (Measure, SnapshotKey, MeasureBand).

Посмотрите на предложение «отсутствует индекс». Он говорит вам, чтобы включить столбцы, чтобы избежать поиска. В более общем случае, если вы ссылаетесь на другие столбцы в своем запросе, они должны быть в ключах или в INCLUDEпредложении нового индекса. В противном случае для получения этих значений все равно потребуется выполнить 595 947 просмотров.

Например, для запроса:

select Measure, SnapshotKey, MeasureBand, SUM(NumLoans), SUM(PrinBal)
from t1
where Measure = 'FinanceFICOScore'
group by Measure, SnapshotKey, MeasureBand

... вам понадобится:

CREATE INDEX ixWhatever 
ON t1 (Measure, SnapshotKey, MeasureBand) 
INCLUDE (NumLoans,PrinBal);
Роб Фарли
источник
6
  1. Поле в вашем условии WHERE не является ведущим полем индекса.

  2. Вы measureопределены как NVARCHAR так префикс буквальный с N: where Measure = N'FinanceFICOScore'.

Рассмотрите возможность создания кластерного индекса на SnapshotKey. Если он уникален, то это может быть PK (и Clustered). Если он не уникален, он не может быть PK, но все же может быть неуникальным кластерным индексом. Тогда ваш некластеризованный индекс будет только по measureстолбцу.

И, учитывая, что первая область в этом GROUP BYтакже measure, это также выиграло бы от того, чтобы measureбыть ведущей областью.

Фактически, для этой операции вам может потребоваться вместо этого определить некластеризованный индекс Measure, SnapshotKey, MeasureBandв том точном порядке, который соответствует GROUP BYпредложению. По размеру, который действительно добавляется только MeasureBandпотому, что индекс NonClustered уже основан Measureи MeasureKeyуже включен в индекс, поскольку теперь он является ключом Clustered Index (нет, Measureне будет дублироваться в индексе NonClustered).

@Rob упомянул в своем удаленном комментарии к своему ответу, что для решения этой проблемы необходимо только определить некластеризованный индекс с этими тремя полями в указанном порядке, и что создание кластерного (неуникального) индекса SnapshotKeyне обязательно . Хотя он, вероятно, и прав (я надеялся, что сработает меньше полей), я все равно утверждаю, что наличие кластерного индекса полезно не только для этой операции, но, вероятно, для большинства других.

Соломон Руцкий
источник
Обсуждение этого ответа было перенесено в чат .
Пол Уайт 9