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