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


14

У мене є запит, який зараз займає в середньому 2500 мс для завершення. Моя таблиця дуже вузька, але є 44 мільйони рядків. Які варіанти я маю для підвищення продуктивності, чи це так добре, наскільки це отримується?

Запит

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

Стіл

CREATE TABLE [dbo].[Heartbeats](
    [ID] [int] IDENTITY(1,1) NOT NULL,
    [DeviceID] [int] NOT NULL,
    [IsPUp] [bit] NOT NULL,
    [IsWebUp] [bit] NOT NULL,
    [IsPingUp] [bit] NOT NULL,
    [DateEntered] [datetime] NOT NULL,
 CONSTRAINT [PK_Heartbeats] PRIMARY KEY CLUSTERED 
(
    [ID] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]

Індекс

CREATE NONCLUSTERED INDEX [CommonQueryIndex] ON [dbo].[Heartbeats] 
(
    [DateEntered] ASC,
    [DeviceID] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]

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

ОНОВЛЕННЯ

Коли я змінюю запит, щоб використовувати підказку індексу сили, запит виконується через 50 мс:

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

Додавання правильно вибіркового пункту DeviceID також відповідає діапазону 50 мс:

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

Якщо додати ORDER BY [DateEntered], [DeviceID]до оригінального запиту, я перебуваю в діапазоні 50 мс:

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

Усі вони використовують індекс, який я очікував (CommonQueryIndex), тому, я вважаю, що зараз у мене питання, чи є спосіб змусити цей індекс використовувати для таких запитів? Або розмір моєї таблиці скидає оптимізатор занадто сильно, і я просто повинен використовувати ORDER BYабо підказку?


Я думаю, ви можете додати ще один некластеризований індекс на "DateEntered", який би дещо збільшив ефективність
Praveen

@Praveen Це був би в основному такий, як мій існуючий індекс? Чи потрібно мені робити щось особливе, оскільки на одному полі будуть два індекси?
Нейт

@Nate, оскільки таблиця називається серцебиттям і в ній задіяно 44 мільйони записів, я припускаю, що у вас є важкі вставки на цьому столі? За допомогою індексації ви можете лише додати індекс покриття для прискорення. Але як ви вже згадували, ви використовуєте цей запит лише зрідка, я б настійно радив цього, якщо ви робите важкі вставки. Це в основному збільшує подвійне завантаження. Ви працюєте на корпоративній редакції?
Едвард Дортленд

Я помітив, що у вашому індексі NC є deviceID. Чи можна включити це до пункту де? І чи це знизить результат, встановлений нижче порогу? <35k записів (без першої статті 1000).
Едвард Дортленд

1
останнє запитання: Ви завжди вставляєте порядок введення дати? Або вони можуть вийти з ладу, оскільки пристрої можуть вставляти асинхронізацію один у одного. Ви можете спробувати змінити кластерний індекс у стовпчик DateEntered. Ваші вихідні сторінки вашого кластерного індексу зараз 445 сторінок. Це подвоїться, якщо ви переходите від int до дати. Але в цьому випадку це може бути не поганим.
Едвард Дортленд

Відповіді:


13

Чому оптимізатор не відповідає вашому першому індексу:

CREATE NONCLUSTERED INDEX [CommonQueryIndex] ON [dbo].[Heartbeats] 
(
    [DateEntered] ASC,
    [DeviceID] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]

Це питання вибірковості стовпця [DateEntered].

Ви сказали нам, що у вашій таблиці 44 мільйони рядків. розмір рядка:

4 байти для ідентифікатора, 4 байти для ідентифікатора пристрою, 8 байт для дати та 1 байт для 4 бітових стовпців. це 17 байт + 7 байт накладних витрат (теги, нульова растрова карта, зміщення вар. кола, кількість кол) становить 24 байти в ряд.

Це грубо перекладається на 140 тис. Сторінок. Зберігати ці 44 мільйони рядків.

Тепер оптимізатор може зробити дві речі:

  1. Він може сканувати таблицю (кластерне сканування індексів)
  2. Або він може використовувати ваш індекс. Після цього для кожного рядка вашого індексу необхідно виконати пошук закладок у кластерному індексі.

Тепер у певний момент просто стає дорожче робити всі ці одиничні пошуки в кластерному індексі для кожного запису індексу, знайденого у вашому некластеризованому індексі. Поріг для цього, як правило, загальна кількість пошукових запитів повинна перевищувати 25% до 33% від загальної кількості сторінок таблиці.

Так що в цьому випадку: 140k / 25% = 35000 рядків 140k / 33% = 46666 рядків.

(@RBarryYoung, 35k - це 0,08% від загальної кількості рядків, а 46666 - 0,10%, тож я думаю, саме там плутанина була)

Тож якщо ваш пункт де призведе десь між 35000 і 46666 рядками (це під верхньою частиною!), Дуже ймовірно, що ваш некластеризований файл не буде використаний і буде застосовано кластерне сканування індексів.

Єдині два способи змінити це:

  1. Зробіть пункт де вибагливішим. (якщо можливо)
  2. Відкиньте * і виберіть лише кілька стовпців, щоб ви могли використовувати індекс покриття.

тепер переконайтеся, що ви можете створити індекс покриття, навіть якщо ви використовуєте select *. Хоча що тільки створює величезні накладні витрати для ваших вставок / оновлень / видалень. Ми повинні знати більше про ваше навантаження на роботу (читати проти запису), щоб переконатися, що це найкраще рішення.

Перехід від дати до мальдате - це зменшення розміру на 16% на кластерному індексі та зменшення розміру на 24% на ваш некластеризований індекс.


поріг сканування, як правило, набагато нижчий від цього (10% або навіть нижчий), однак, оскільки діапазон - це один день, який перевищує рік тому, він не повинен складати навіть цього порогу. І кластерне сканування індексів не є заданим, оскільки додано індекс покриття. Оскільки цей індекс робить пункт WHERE спроможним SARG, його слід віддати перевагу.
RBarryYoung

@RBarryYoung Я намагався пояснити, чому некластеризований індекс на [EnteredDate], [DeviceID] не використовується в першу чергу. Щодо порогу, я думаю, що ми обидва погоджуємось, я говорю лише з точки зору сторінки. Я зміню свою відповідь, щоб зробити це більш зрозумілим.
Едвард Дортленд

Змінив відповідь, щоб зрозуміти, на що я відповідав. Я не можу пояснити, чому індекс покриття, запропонований @RBarryYoung, не використовується. Я тестував його на мільйон рядків саме тут, і оптимізував його за допомогою індексу покриття.
Едвард Дортленд

Дякую за дуже всебічну відповідь, має багато сенсу. Що стосується навантаження, то таблиця містить 150-300 вставок за 5 хвилин і кілька читань на день для цілей звітування.
Нейт

Накладні головки для індексу покриття не дуже важливі, враховуючи, що це вузька таблиця, а "покриття" - лише доповнення до раніше існуючого індексу, який вже включав більшу частину рядка.
RBarryYoung

8

Чи є певна причина того, що ваш ПК є кластеризованим? Багато людей роблять це, тому що це налаштовано за замовчуванням, або вони думають, що ПК повинні бути згруповані. Ні. Кластеризовані індекси, як правило, найкращі для запитів діапазону (наприклад, цей) або в зовнішньому ключі дочірньої таблиці.

Ефект кластерного індексу полягає в тому, що він об'єднує всі дані разом, оскільки дані зберігаються на вузлах листів дерева кластера b. Отже, припускаючи, що ви не запитуєте "занадто широкий" діапазон, оптимізатор точно дізнається, яка частина дерева b містить дані, і йому не доведеться знаходити ідентифікатор рядка, а потім переходити до місця, де дані є (як це робиться при роботі з індексом NC). Що таке "занадто широкий" діапазон? Смішним прикладом може бути запит даних за 11 місяців із таблиці, у якій є лише записи на рік. Отримання даних за один день не повинно бути проблемою, якщо припустити, що ваша статистика є актуальною. (Хоча оптимізатор може потрапити в проблеми, якщо ви шукаєте вчорашні дані та не оновлювали статистику протягом трьох днів.)

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

Отже, що робити?

Моя пропозиція полягатиме в тому, щоб скинути індекс NC, змінити кластеризований ПК на некластеризований та створити кластерний індекс на [DateEntered]. Простіше - краще, поки не буде доведено інше.


Якщо припустити, що рядки вставляються у порядку зростання, це найпростіша відповідь - але вставка в нелінійному порядку спричинить фрагментацію.
Кірк Бродхерст

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

4

Поки у вас є "*", то єдине, що я міг би уявити, що мало би велике значення, - це змінити ваше визначення індексу на це:

CREATE NONCLUSTERED INDEX [CommonQueryIndex] ON [dbo].[Heartbeats] 
(
    [DateEntered] ASC,
    [DeviceID] ASC
)INCLUDE (ID, IsWebUp, IsPingUp, IsPUp)
 WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]

Як я зазначав у коментарях, він повинен використовувати цей індекс, але якщо він відсутній, ви можете переконати його або ЗАМОВИТИ БУКУ, або підказку індексу.


Я просто спробував це, і я все ще майже все те саме місце, 2500мс чекаю відповіді сервера та 10мс клієнтського процесу.
Нейт

Опублікуйте план запитів.
RBarryYoung

Схоже, він використовує індекс кластера. (SELECT Вартість: 0% <- Найвища вартість: 20% <- Кластерне сканування індексу PK_Heartbeats Вартість: 80%)
Nate

Так, це не так, щось викидає статистику / оптимізатор. Додайте підказку, щоб змусити її використовувати новий індекс.
RBarryYoung

@Max Vernon: Можливо, але це повинно бути позначено на плані запитів.
RBarryYoung

3

Я би на це трохи по-іншому дивився.

  • Так, я знаю, що це стара нитка, але мене заінтригує.

Я скидаю стовпець дата - змініть його на int. Майте таблицю пошуку або робіть перетворення для своєї дати.

Вивантажте кластерний індекс - залиште його як купу і створіть некластеризований індекс у новому стовпці INT, який представляє дату. тобто сьогодні буде 20121015. Цей порядок важливий. Залежно від того, як часто ви завантажуєте таблицю, подивіться на створення цього індексу в порядку DESC. Вартість Maint буде вище, і вам потрібно буде ввести коефіцієнт заповнення або розподіл. Розмежування також допоможе скоротити час виконання.

Нарешті, якщо ви можете використовувати SQL 2012, спробуйте використовувати SEQUENCE - це перевершить ідентифікацію () для вставок.


Цікаве рішення. Хоча це не очевидно з мого запитання, часова частина DateTime є дуже важливою. Як правило, я запитую на основі дати, щоб переглянути конкретний час у цей період. Як би ви налаштували це рішення для обліку цього?
Нейт

У такому випадку зберігайте стовпець datetime, додайте колонку int для дати (оскільки діапазон базується на елементі дати, а не на елементі часу). Ви також можете скористатися типом даних TIME, а потім ефективно розділити час окремо від дати. Таким чином, ваш показник даних менший, і у вас все ще є елемент Час стовпця.
Джеремі Лоуелл

1
Я не впевнений, чому я пропустив це раніше, але використовую стиснення рядків на кластерному індексі та некластеризованому індексі. Я просто зробив швидкий тест з вашою таблицею, і ось що я знайшов: я створив набір даних (5,8 мільйона рядків) у таблиці, визначеній вище. Я стиснув (рядок) кластерний та некластеризований індекс. логічні показники, засновані на вашому точному запиті, зменшилися з 2 074 до 1433. Це суттєве зменшення, і я впевнений, що поодинці вам допоможуть - і це дуже низький ризик.
Джеремі Лоуелл
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.