Чому цей запит не використовує мій некластеризований індекс, і як його зробити?


12

Надаючи відповідь на це питання щодо підвищення продуктивності запитів, я хотів би дізнатися, чи є спосіб зробити свій індекс за замовчуванням.

Цей запит запускається приблизно за 2,5 секунди:

SELECT TOP 1000 * FROM [CIA_WIZ].[dbo].[Heartbeats]
WHERE [DateEntered] BETWEEN '2011-08-30' and '2011-08-31';

Цей працює приблизно за 33 мс:

SELECT TOP 1000 * FROM [CIA_WIZ].[dbo].[Heartbeats]
WHERE [DateEntered] BETWEEN '2011-08-30' and '2011-08-31' 
ORDER BY [DateEntered], [DeviceID];

У полі [ID] (pk) є кластерний індекс, а індекс, некластеризований на [DateEntered], [DeviceID]. Перший запит використовує кластерний індекс, другий запит використовує мій некластеризований індекс. Моє запитання складається з двох частин:

  • Чому, оскільки обидва запити мають пункт WHERE у полі [DateEntered], сервер використовує кластерний індекс для першого, а не другого?
  • Як я можу змусити некластеризований індекс використовувати за замовчуванням у цьому запиті навіть без порядку замовлення? (Або чому я не хотів би такої поведінки?)

DateEntered - це DateTime, в цьому випадку я використовую частину дати, але іноді я запитую як дату, так і час разом.
Нейт

Відповіді:


9

Перший запит виконує сканування таблиці на основі порогу, про який я пояснював раніше: Чи можна збільшити ефективність запиту у вузькій таблиці з мільйонами рядків?

(швидше за все, ваш запит без цього TOP 1000пункту поверне більше 46k рядків або десь між 35k та 46k. (сіра зона ;-))

другий запит, повинен бути замовлений. Оскільки вам індекс NC впорядковується у потрібному порядку, оптимізатору дешевше використовувати цей індекс, а потім переглядати закладки до кластеризованого індексу, щоб отримати відсутні стовпці порівняно з кластерним скануванням індексів та необхідністю. щоб замовити це.

змінити порядок стовпців у ORDER BYпункті, і ви повернетесь до кластерного сканування індексу, оскільки NC INDEX потім марний.

редагувати забув відповідь на друге запитання, чому ти цього не хочеш

Використання некластерного індексу, що не охоплює, означає, що rowID шукається в індексі NC, а потім пропущені стовпці потрібно шукати в кластерному індексі (кластерний індекс містить усі стовпці таблиці). IO для пошуку відсутніх стовпців у кластерному індексі - це випадкові IO.

Ключовим тут є RANDOM. оскільки для кожного рядка, знайденого в індексі NC, методи доступу повинні шукати нову сторінку в кластерному індексі. Це випадково, а тому дуже дорого.

Тепер, з іншого боку, оптимізатор також може піти на кластерне сканування індексу. Він може використовувати карти розподілу для пошуку діапазонів сканування і просто починати читати індекс кластеру великими шматками. Це послідовно і значно дешевше. (поки ваша таблиця не фрагментована :-)) Мінус - цілий кластерний індекс потрібно прочитати. Це погано для вашого буфера і, можливо, величезної кількості вводу-виводу. але все ж послідовні введення-виведення.

У вашому випадку оптимізатор вирішує десь між 35k і 46k рядків, це менш дорого, ніж повне кластерне сканування індексів. Так, це неправильно І у багатьох випадках із вузькими некластеризованими індексами, що не мають вибіркових WHEREпропозицій чи великою таблицею, з цього приводу це йде не так. (Ваш стіл гірший, тому що це теж дуже вузький стіл.)

Тепер додавання ORDER BYробить дорогішим сканування повного кластерного індексу, а потім замовлення результатів. Натомість оптимізатор припускає, що дешевше використовувати вже замовлений індекс NC, а потім сплатити випадковий IO штраф за пошук закладок.

Тож ваше замовлення - це ідеальне рішення "підказки запиту". Але в певний момент, коли результати ваших запитів будуть такими великими, штраф за випадкові IO пошуку закладок буде настільки великим, що він стає повільніше. Я припускаю, що оптимізатор змінить плани назад до кластерного сканування індексу до цього моменту, але ви ніколи не знаєте точно.

У вашому випадку, доки ваші вставки впорядковані enterdate, як це обговорювалося в чаті та попередньому питанні (див. Посилання), вам краще створити кластерний індекс у стовпці enterDate.


20

Вираження запиту за допомогою різного синтаксису іноді може допомогти повідомити оптимізатору ваше бажання використовувати некластеризований індекс. Ви повинні знайти форму, яка надає план, який ви хочете:

SELECT
    [ID],
    [DeviceID],
    [IsPUp],
    [IsWebUp],
    [IsPingUp],
    [DateEntered]
FROM [dbo].[Heartbeats]
WHERE
    [ID] IN
(
    -- Keys
    SELECT TOP (1000)
        [ID]
    FROM [dbo].[Heartbeats]
    WHERE 
        [DateEntered] >= CONVERT(datetime, '2011-08-30', 121)
        AND [DateEntered]  < CONVERT(datetime, '2011-08-31', 121)
);

План запитів

Порівняйте цей план із тим, що складається, коли некластеризований індекс вимушений із підказкою:

SELECT TOP (1000) 
    * 
FROM [dbo].[Heartbeats] WITH (INDEX(CommonQueryIndex))
WHERE 
    [DateEntered] BETWEEN '2011-08-30' and '2011-08-31';

План примусового підказки

По суті плани однакові (пошук ключів - це не що інше, як пошук кластерного індексу). Обидві форми плану виконуватимуть лише один пошук за некластеризованим індексом та максимум 1000 пошукових даних у кластерному індексі.

Важлива відмінність полягає в позиції оператора Top. Розташований між двома прагненнями, Top запобігає оптимізатору замінити дві пошукові операції логічно-еквівалентним скануванням кластерного індексу. Оптимізатор працює, замінюючи частини логічного плану на еквівалентні реляційні операції. Top не є реляційним оператором, тому перезапис запобігає перетворенню в кластерне сканування індексів. Якщо оптимізатору вдалося змінити позицію оператора Top, він все-таки віддасть перевагу скануванню через пошук + пошук через те, як працює оцінка витрат.

Витрати сканувань та пошуків

На дуже високому рівні модель витрат оптимізатора на сканування та пошук досить проста: за підрахунками, 320 випадкових пошуків коштують стільки ж, скільки читання 1350 сторінок при скануванні. Це, мабуть, мало нагадує апаратні можливості будь-якої конкретної сучасної системи вводу / виводу, але це працює досить добре, як практична модель.

Модель також робить ряд спрощених припущень, головне з яких полягає в тому, що кожен запит повинен починатися без даних або покажчикових сторінок, які вже є в кеші. Це означає, що кожне введення-виведення призведе до фізичного вводу / виводу - хоча це на практиці рідко буває. Навіть із холодним кешем попереднє вилучення та попереднє читання означають, що потрібні сторінки насправді є цілком ймовірними у пам’яті до моменту, коли потрібен процесор запитів.

Інша думка полягає в тому, що перший запит рядка, який не є в пам'яті, призведе до того, що вся сторінка буде виймана з диска. Подальші запити на рядки на одній сторінці, швидше за все, не матимуть фізичного вводу / виводу. Модель калькуляції дійсно містить логіку для врахування таких ефектів, але це не є ідеальною.

Усі ці речі (і більше) означають, що оптимізатор має тенденцію перейти до сканування раніше, ніж, мабуть, слід. Випадкові введення / виведення є лише "набагато дорожче", ніж "послідовне" введення / виведення, якщо результат фізичної операції - доступ до сторінок в пам'яті дійсно дуже швидкий. Навіть там, де потрібне фізичне зчитування, сканування може взагалі не призвести до послідовного зчитування через фрагментацію, і прагнення можуть бути розміщені таким чином, що шаблон є по суті послідовним. Додайте до цього зміна продуктивності, характерна для сучасних систем вводу / виводу (особливо твердотільних), і вся справа починає виглядати дуже хиткою.

Рядові цілі

Наявність у плані оператора Top модифікує підхід до витрат. Оптимізатор досить розумний, щоб знати, що для пошуку 1000 рядків за допомогою сканування, швидше за все, не знадобиться сканування всього кластерного індексу - він може зупинитися, як тільки знайдеться 1000 рядків. Він встановлює 'ціль рядка' у 1000 рядків у оператора Top і використовує статистичну інформацію для опрацювання звідти, щоб оцінити, скільки рядків потрібно очікувати від джерела рядка (сканування в цьому випадку). Про деталі цього розрахунку я писав тут .

Зображення в цій відповіді були створені за допомогою SQL Sentry Plan Explorer .

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