Чому мій індекс не використовується в 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 одиниці, здебільшого тому, що вона очікує, щоб прочитати 5515 рядків з індексу, сортувати їх і повернути ту, яка сортується найнижчо (за 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;

Об’єднання інтервалу та сортування


5

Індекс здається досить хорошим для запиту, і я не впевнений, чому його не вибирає оптимізатор (статистика? Розбиття на розбиття? Обмеження лазуру ?, Насправді ідеї немає).

Але відфільтрований індекс буде ще кращим для конкретного запиту, якщо значення > 0є фіксованим значенням і не змінюється від одного виконання запиту до іншого:

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

Існує дві різниці між індексом, який у вас є, де OperationalSecondsзнаходиться 3-й стовпець, і відфільтрованим індексом:

  • Спочатку відфільтрований показник менший, як по ширині (вужчій), так і по кількості рядків.
    Це робить відфільтрований індекс в цілому більш ефективним, оскільки SQL серверу потрібно менше місця, щоб зберегти його в пам'яті.

  • По-друге, і це є більш тонким і важливим для запиту є те, що він містить лише рядки, які відповідають фільтру, використовуваному в запиті. Це може бути надзвичайно важливим, залежно від значень цього 3-го стовпця.
    Наприклад, певний набір параметрів для MachineryIdі DateRecordedможе дати 1000 рядків. Якщо всі або майже всі ці рядки відповідають (OperationalSeconds > 0)фільтру, обидва індекси будуть добре працювати. Але якщо рядків, що відповідають фільтру, дуже мало (або лише останній або взагалі відсутні), перший індекс повинен буде пройти через багато або всі 1000 рядків, поки він не знайде збіг. З іншого боку, відфільтрований індекс потребує лише однієї спроби знайти відповідний рядок (або повернути 0 рядків), оскільки зберігаються лише рядки, що відповідають фільтру.


1
Чи додавання індексу зробило запит більш ефективним?
ypercubeᵀᴹ

Якщо не базу даних, то для перевірки належним чином потрібно більше даних, але я ще не пробував це в прямому ефірі, нові індекси займають більше години, щоб розвивати цю. Я також дуже вагаюся зробити що-небудь з нашою базою даних, оскільки вона вже працює повільно. Нам потрібна краща система для клонування нашого життя в постановку.
Ендрю Вільямсон
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.