Вот краткий обзор: я делаю запрос на выборку. Каждый столбец в 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 , поэтому я просто собираюсь сделать хранимую процедуру.
источник
Ответы:
Этот индекс не разбит на разделы, поэтому оптимизатор распознает его, который можно использовать для обеспечения порядка, указанного в запросе, без сортировки. Как неуникальный некластеризованный индекс, он также имеет ключи кластерного индекса в качестве подразделов, поэтому индекс можно использовать для поиска
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
):Этот индекс секционирован и не может возвращать строки по
DateRecorded
порядку напрямую (см. Далее ). Он может искатьMachineryId
иDateRecorded
диапазон внутри каждого раздела , но требуется сортировка:Если бы этот индекс не был секционирован, сортировка не потребовалась бы, и он был бы очень похож на другой (неразделенный) индекс с дополнительным включенным столбцом. Нераспределенный отфильтрованный индекс будет еще более эффективным.
Вы должны обновить запрос источника , так что типы данных этих
@From
и@To
параметров совпадают сDateRecorded
колонкой (datetime
). В настоящий момент SQL Server вычисляет динамический диапазон из-за несоответствия типов во время выполнения (используя оператор Merge Interval и его поддерево):Это преобразование не позволяет оптимизатору правильно рассуждать о взаимосвязи между идентификаторами восходящих разделов (охватывающих диапазон
DateRecorded
значений в порядке возрастания) и предикатами неравенстваDateRecorded
.Идентификатор раздела - это неявный ведущий ключ для многораздельного индекса. Как правило, оптимизатор может видеть, что упорядочение по идентификатору раздела (где восходящие идентификаторы соответствуют восходящим, непересекающимся значениям
DateRecorded
) тогдаDateRecorded
совпадает с упорядочением поDateRecorded
одному (при условии, что оноMachineryID
является постоянным). Эта цепочка рассуждений нарушается преобразованием типов.демонстрация
Простая секционированная таблица и индекс:
Запрос с подходящими типами
Запрос с несовпадающими типами
источник
Индекс кажется довольно хорошим для запроса, и я не уверен, почему он не выбран оптимизатором (статистика? Разбиение? Ограничение лазури?, Понятия не имею).
Но отфильтрованный индекс будет даже лучше для конкретного запроса, если он
> 0
является фиксированным значением и не изменяется от одного выполнения запроса к другому:Существует два различия между индексом, в котором
OperationalSeconds
находится третий столбец, и фильтрованным индексом:Сначала отфильтрованный индекс меньше как по ширине (уже), так и по количеству строк.
Это делает отфильтрованный индекс в целом более эффективным, поскольку SQL Server требуется меньше места для его хранения в памяти.
Во-вторых, это более тонкий и важный для запроса тот факт, что в нем есть только те строки, которые соответствуют фильтру, используемому в запросе. Это может быть чрезвычайно важно, в зависимости от значений этого третьего столбца.
Например, определенный набор параметров для
MachineryId
иDateRecorded
может дать 1000 строк. Если все или почти все эти строки соответствуют(OperationalSeconds > 0)
фильтру, оба индекса будут вести себя хорошо. Но если строк, соответствующих фильтру, очень мало (или только последняя, или ни одной), первый индекс должен будет пройти много или все эти 1000 строк, пока не найдет совпадение. Отфильтрованному индексу, с другой стороны, требуется только один поиск, чтобы найти совпадающую строку (или вернуть 0 строк), поскольку хранятся только строки, соответствующие фильтру.источник