Почему мой индекс не используется в SELECT TOP?

15

Вот краткий обзор: я делаю запрос на выборку. Каждый столбец в WHEREи ORDER BYпунктах находятся в одном некластерном индекс IX_MachineryId_DateRecorded, либо как часть ключа, или в качестве INCLUDEстолбцов. Я выбираю все столбцы, так что это приведет к поиску закладок, но я только беру TOP (1), так что сервер может сказать, что поиск нужно выполнить только один раз, в конце.

Самое главное, когда я заставляю запрос использовать индекс IX_MachineryId_DateRecorded, он выполняется менее чем за секунду. Если я позволю серверу решить, какой индекс использовать, он выбирает IX_MachineryId, и это занимает до минуты. Это действительно наводит на мысль, что я правильно сделал индекс, а сервер просто принимает неверное решение. Почему?

CREATE TABLE [dbo].[MachineryReading] (
    [Id]                 INT              IDENTITY (1, 1) NOT NULL,
    [Location]           [sys].[geometry] NULL,
    [Latitude]           FLOAT (53)       NOT NULL,
    [Longitude]          FLOAT (53)       NOT NULL,
    [Altitude]           FLOAT (53)       NULL,
    [Odometer]           INT              NULL,
    [Speed]              FLOAT (53)       NULL,
    [BatteryLevel]       INT              NULL,
    [PinFlags]           BIGINT           NOT NULL,
    [DateRecorded]       DATETIME         NOT NULL,
    [DateReceived]       DATETIME         NOT NULL,
    [Satellites]         INT              NOT NULL,
    [HDOP]               FLOAT (53)       NOT NULL,
    [MachineryId]        INT              NOT NULL,
    [TrackerId]          INT              NOT NULL,
    [ReportType]         NVARCHAR (1)     NULL,
    [FixStatus]          INT              DEFAULT ((0)) NOT NULL,
    [AlarmStatus]        INT              DEFAULT ((0)) NOT NULL,
    [OperationalSeconds] INT              DEFAULT ((0)) NOT NULL,
    CONSTRAINT [PK_dbo.MachineryReading] PRIMARY KEY CLUSTERED ([Id] ASC),
    CONSTRAINT [FK_dbo.MachineryReading_dbo.Machinery_MachineryId] FOREIGN KEY ([MachineryId]) REFERENCES [dbo].[Machinery] ([Id]) ON DELETE CASCADE,
    CONSTRAINT [FK_dbo.MachineryReading_dbo.Tracker_TrackerId] FOREIGN KEY ([TrackerId]) REFERENCES [dbo].[Tracker] ([Id]) ON DELETE CASCADE
);

GO
CREATE NONCLUSTERED INDEX [IX_MachineryId]
    ON [dbo].[MachineryReading]([MachineryId] ASC);

GO
CREATE NONCLUSTERED INDEX [IX_TrackerId]
    ON [dbo].[MachineryReading]([TrackerId] ASC);

GO
CREATE NONCLUSTERED INDEX [IX_MachineryId_DateRecorded]
    ON [dbo].[MachineryReading]([MachineryId] ASC, [DateRecorded] ASC)
    INCLUDE([OperationalSeconds], [FixStatus]);

Таблица разбита на месячные диапазоны (хотя я до сих пор не понимаю, что там происходит).

ALTER PARTITION SCHEME PartitionSchemeMonthRange NEXT USED [Primary]
ALTER PARTITION FUNCTION [PartitionFunctionMonthRange]() SPLIT RANGE(N'2016-01-01T00:00:00.000') 

ALTER PARTITION SCHEME PartitionSchemeMonthRange NEXT USED [Primary]
ALTER PARTITION FUNCTION [PartitionFunctionMonthRange]() SPLIT RANGE(N'2016-02-01T00:00:00.000') 
...

CREATE UNIQUE CLUSTERED INDEX [PK_dbo.MachineryReadingPs] ON MachineryReading(DateRecorded, Id) ON PartitionSchemeMonthRange(DateRecorded)

Запрос, который я обычно запускаю:

SELECT TOP (1) [Id], [Location], [Latitude], [Longitude], [Altitude], [Odometer], [ReportType], [FixStatus], [AlarmStatus], [Speed], [BatteryLevel], [PinFlags], [DateRecorded], [DateReceived], [Satellites], [HDOP], [OperationalSeconds], [MachineryId], [TrackerId]
    FROM [dbo].[MachineryReading]
    --WITH(INDEX(IX_MachineryId_DateRecorded)) --This makes all the difference
    WHERE ([MachineryId] = @p__linq__0) AND ([DateRecorded] >= @p__linq__1) AND ([DateRecorded] < @p__linq__2) AND ([OperationalSeconds] > 0)
    ORDER BY [DateRecorded] ASC

План запроса: https://www.brentozar.com/pastetheplan/?id=r1c-RpxNx

План запроса с принудительным индексом: https://www.brentozar.com/pastetheplan/?id=SywwTagVe

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

У меня такое ощущение, что это из-за разбиения, и мой запрос обычно охватывает каждый отдельный раздел (например, когда я хочу получить первый или последний OperationalSecondsзаписанный файл для одной машины). Однако все запросы, которые я писал вручную, работают в 10–100 раз быстрее, чем сгенерированные EntityFramework , поэтому я просто собираюсь сделать хранимую процедуру.

Эндрю Уильямсон
источник
1
Привет @AndrewWilliamson, это может быть проблема статистики. Если вы видите фактический план из нефорсированного плана, предполагаемое количество строк равно 1,22, а фактическое - 19039. Это, в свою очередь, приводит к поиску ключа, который вы увидите позже в плане. Вы пытались обновить статистику? Если нет, попробуйте выполнить полное сканирование промежуточной базы данных.
jesijesi

Ответы:

21

Если я позволю серверу решить, какой индекс использовать, он выбирает IX_MachineryId, и это занимает до минуты.

Этот индекс не разбит на разделы, поэтому оптимизатор распознает его, который можно использовать для обеспечения порядка, указанного в запросе, без сортировки. Как неуникальный некластеризованный индекс, он также имеет ключи кластерного индекса в качестве подразделов, поэтому индекс можно использовать для поиска MachineryIdи DateRecordedдиапазона:

Поиск индекса

Индекс не включает OperationalSeconds, поэтому план должен искать это значение на строку в (многораздельном) кластерном индексе, чтобы проверить OperationalSeconds > 0:

Погляди

Оптимизатор оценивает, что одна строка должна быть прочитана из некластеризованного индекса и найдена, чтобы удовлетворить TOP (1) . Этот расчет основан на цели строки (быстро найти одну строку) и предполагает равномерное распределение значений.

Из фактического плана, мы видим, что оценка 1 строки является неточной. Фактически, нужно обработать 19 039 строк, чтобы обнаружить, что ни одна строка не удовлетворяет условиям запроса. Это наихудший случай для оптимизации цели строки (оценивается 1 строка, все строки действительно нужны):

Фактическая / оценка

Вы можете отключить цели строк с помощью флага трассировки 4138 . Это, скорее всего, приведет к тому, что SQL Server выберет другой план, возможно, тот, который вы заставили. В любом случае индекс IX_MachineryIdможно сделать более оптимальным путем включения OperationalSeconds.

Весьма необычно иметь невыровненные некластеризованные индексы (индексы, разделенные по-разному от базовой таблицы, в том числе и вовсе).

Это действительно наводит на мысль, что я правильно сделал индекс, а сервер просто принимает неверное решение. Почему?

Как обычно, оптимизатор выбирает самый дешевый план, который он считает.

Ориентировочная стоимость IX_MachineryIdплана составляет 0,01 единицы стоимости, исходя из предположения о (неверной) цели строки, что одна строка будет проверена и возвращена.

Ориентировочная стоимость IX_MachineryId_DateRecordedплана намного выше, 0,27 единицы, в основном потому, что он ожидает прочитать 5 515 строк из индекса, отсортировать их и вернуть ту, которая сортирует наименьшую (по DateRecorded):

Топ N Сортировать

Этот индекс секционирован и не может возвращать строки по DateRecordedпорядку напрямую (см. Далее ). Он может искать MachineryIdи DateRecordedдиапазон внутри каждого раздела , но требуется сортировка:

Разделенный поиск

Если бы этот индекс не был секционирован, сортировка не потребовалась бы, и он был бы очень похож на другой (неразделенный) индекс с дополнительным включенным столбцом. Нераспределенный отфильтрованный индекс будет еще более эффективным.


Вы должны обновить запрос источника , так что типы данных этих @Fromи @Toпараметров совпадают с DateRecordedколонкой ( datetime). В настоящий момент SQL Server вычисляет динамический диапазон из-за несоответствия типов во время выполнения (используя оператор Merge Interval и его поддерево):

<ScalarOperator ScalarString="GetRangeWithMismatchedTypes([@From],NULL,(22))">
<ScalarOperator ScalarString="GetRangeWithMismatchedTypes([@To],NULL,(22))">

Это преобразование не позволяет оптимизатору правильно рассуждать о взаимосвязи между идентификаторами восходящих разделов (охватывающих диапазон DateRecordedзначений в порядке возрастания) и предикатами неравенстваDateRecorded .

Идентификатор раздела - это неявный ведущий ключ для многораздельного индекса. Как правило, оптимизатор может видеть, что упорядочение по идентификатору раздела (где восходящие идентификаторы соответствуют восходящим, непересекающимся значениям DateRecorded) тогда DateRecordedсовпадает с упорядочением по DateRecordedодному (при условии, что оно MachineryIDявляется постоянным). Эта цепочка рассуждений нарушается преобразованием типов.

демонстрация

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

CREATE PARTITION FUNCTION PF (datetime)
AS RANGE LEFT FOR VALUES ('20160101', '20160201', '20160301');

CREATE PARTITION SCHEME PS AS PARTITION PF ALL TO ([PRIMARY]);

CREATE TABLE dbo.T (c1 integer NOT NULL, c2 datetime NOT NULL) ON PS (c2);

CREATE INDEX i ON dbo.T (c1, c2) ON PS (c2);

INSERT dbo.T (c1, c2) 
VALUES (1, '20160101'), (1, '20160201'), (1, '20160301');

Запрос с подходящими типами

-- Types match (datetime)
DECLARE 
    @From datetime = '20010101',
    @To datetime = '20090101';

-- Seek with no sort
SELECT T2.c2 
FROM dbo.T AS T2 
WHERE T2.c1 = 1 
AND T2.c2 >= @From
AND T2.c2 < @To
ORDER BY 
    T2.c2;

Не искать

Запрос с несовпадающими типами

-- Mismatched types (datetime2 vs datetime)
DECLARE 
    @From datetime2 = '20010101',
    @To datetime2 = '20090101';

-- Merge Interval and Sort
SELECT T2.c2 
FROM dbo.T AS T2 
WHERE T2.c1 = 1 
AND T2.c2 >= @From
AND T2.c2 < @To
ORDER BY 
    T2.c2;

Объединить интервал и сортировать

Пол Уайт 9
источник
5

Индекс кажется довольно хорошим для запроса, и я не уверен, почему он не выбран оптимизатором (статистика? Разбиение? Ограничение лазури?, Понятия не имею).

Но отфильтрованный индекс будет даже лучше для конкретного запроса, если он > 0является фиксированным значением и не изменяется от одного выполнения запроса к другому:

CREATE NONCLUSTERED INDEX IX_MachineryId_DateRecorded_filtered
    ON dbo.MachineryReading
        (MachineryId, DateRecorded) 
    WHERE (OperationalSeconds > 0) ;

Существует два различия между индексом, в котором OperationalSecondsнаходится третий столбец, и фильтрованным индексом:

  • Сначала отфильтрованный индекс меньше как по ширине (уже), так и по количеству строк.
    Это делает отфильтрованный индекс в целом более эффективным, поскольку SQL Server требуется меньше места для его хранения в памяти.

  • Во-вторых, это более тонкий и важный для запроса тот факт, что в нем есть только те строки, которые соответствуют фильтру, используемому в запросе. Это может быть чрезвычайно важно, в зависимости от значений этого третьего столбца.
    Например, определенный набор параметров для MachineryIdи DateRecordedможет дать 1000 строк. Если все или почти все эти строки соответствуют (OperationalSeconds > 0)фильтру, оба индекса будут вести себя хорошо. Но если строк, соответствующих фильтру, очень мало (или только последняя, ​​или ни одной), первый индекс должен будет пройти много или все эти 1000 строк, пока не найдет совпадение. Отфильтрованному индексу, с другой стороны, требуется только один поиск, чтобы найти совпадающую строку (или вернуть 0 строк), поскольку хранятся только строки, соответствующие фильтру.

ypercubeᵀᴹ
источник
1
Повысило ли эффективность добавления запроса индекс?
ypercubeᵀᴹ
Не для промежуточной базы данных (для правильной проверки ей действительно нужно больше данных), я еще не пробовал ее в живую, для создания новых индексов требуется более часа. Я также не решаюсь что-либо делать с нашей действующей базой данных, поскольку она уже работает медленно. Нам нужна лучшая система для клонирования нашей жизни в постановку.
Эндрю Уильямсон