Оптимізація приєднання на великому столі


10

Я намагаюся отримати ще одну ефективність із запиту, який отримує доступ до таблиці з ~ 250 мільйонами записів. З мого читання фактичного (не оціненого) плану виконання, перше вузьке місце - це запит, який виглядає приблизно так:

select
    b.stuff,
    a.added,
    a.value
from
    dbo.hugetable a
    inner join
    #smalltable b on a.fk = b.pk
where
    a.added between @start and @end;

Далі див. Нижче визначення визначних таблиць та індексів.

План виконання вказує, що вкладений цикл використовується на #smalltable, і що сканування індексу над величезним таблицею виконується 480 разів (для кожного рядка в #smalltable). Це здається мені назад, тому я намагався змусити замість цього об'єднати об'єднання:

select
    b.stuff,
    a.added,
    a.value
from
    dbo.hugetable a with(index = ix_hugetable)
    inner merge join
    #smalltable b with(index(1)) on a.fk = b.pk
where
    a.added between @start and @end;

Індекс, про який йдеться (див. Нижче для повного визначення), охоплює стовпці fk (предикат приєднання), доданий (використовується у пункті де) та id (непотрібний) у порядку зростання та включає значення .

Однак, коли я це роблю, запит закінчується від 2 1/2 хвилини до більше 9. Я би сподівався, що підказки змусять більш ефективно приєднатись, що робить лише один прохід над кожною таблицею, але явно ні.

Будь-які вказівки вітаються. За необхідності надається додаткова інформація.

Оновлення (2011/06/02)

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

select
    b.stuff,
    datediff(month, 0, a.added),
    count(a.value),
    sum(case when a.value > 0 else 1 end) -- this triples the running time!
from
    dbo.hugetable a
    inner join
    #smalltable b on a.fk = b.pk
group by
    b.stuff,
    datediff(month, 0, a.added);

В даний час величезний таблиця має кластерний індекс pk_hugetable (added, fk)(первинний ключ), а некластеризований індекс - іншим шляхом ix_hugetable (fk, added).

Без четвертого стовпця вище оптимізатор використовує вкладене вкладене цикл, як і раніше, використовуючи #smalltable як зовнішній вхід, а некластеризований індекс шукають як внутрішній цикл (виконуючи ще раз 480 разів). Що мене хвилює - це невідповідність між орієнтовними рядками (12 958,4) та фактичними рядками (74 668 468). Відносна вартість цих прагнень становить 45%. Однак час роботи триває менше хвилини.

За допомогою 4-го стовпця час роботи збільшується до 4 хвилин. Цього разу він шукає на кластерному індексі (2 виконання) за однакову відносну вартість (45%), агрегує через хеш-матч (30%), потім робить хеш-з'єднання на #smalltable (0%).

Я не впевнений, що стосується мого наступного курсу дій. Мене хвилює те, що не гарантується ні пошук діапазону дат, ні присудок приєднання, або навіть все, що може різко зменшити набір результатів. Діапазон дат у більшості випадків обріже лише 10-15% записів, а внутрішнє з'єднання на fk може відфільтрувати, можливо, 20-30%.


Як вимагає Уілл А, результати sp_spaceused:

name      | rows      | reserved    | data        | index_size  | unused
hugetable | 261774373 | 93552920 KB | 18373816 KB | 75167432 KB | 11672 KB

#smalltable визначається як:

create table #endpoints (
    pk uniqueidentifier primary key clustered,
    stuff varchar(6) null
);

Тоді як dbo.hugetable визначається як:

create table dbo.hugetable (
    id uniqueidentifier not null,
    fk uniqueidentifier not null,
    added datetime not null,
    value decimal(13, 3) not null,

    constraint pk_hugetable primary key clustered (
        fk asc,
        added asc,
        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 ix_hugetable on dbo.hugetable (
    fk asc, added asc, id asc
) include(value) 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];

Поле id є зайвим - артефакт попередньої DBA, який наполягав на тому, що всі таблиці скрізь повинні мати GUID, не виняток.


Чи можете ви включити результат sp_spaceused 'dbo.hugetable', будь ласка?
Чи

Готово, додано трохи вище початку визначення таблиці.
Швидкий Джо Сміт

Це точно. Його смішні розміри - це причина, чому я розглядаю це.
Швидкий Джо Сміт

Відповіді:


5

Ви ix_hugetableвиглядаєте досить марно, оскільки:

  • він є кластерним індексом (ПК)
  • INCLUDE не має ніякої різниці, оскільки кластерний індекс ВКЛЮЧУЄ всі неклавішні стовпці (не ключові значення в нижньому аркуші = INCLUDEd = що таке кластерний індекс)

Крім того: - додано або fk має бути першим - ідентифікатор є першим = мало використовуйте

Спробуйте змінити кластеризований ключ на (added, fk, id)і скинути ix_hugetable. Ви вже пробували (fk, added, id). Якщо нічого іншого, ви заощадите багато дискового простору та обслуговування індексу

Іншим варіантом може бути спробувати підказку FORCE ORDER з набором порядку замовлення таблиць і без підказів JOIN / INDEX. Я намагаюся особисто не використовувати підказки JOIN / INDEX, оскільки ви видаляєте параметри оптимізатора. Багато років тому мені сказали (семінар з гуру SQL), що підказка FORCE ORDER може допомогти, коли у вас є величезна таблиця ПРИЄДНАЙТЕСЬ невелику таблицю: YMMV 7 років потому ...

О, і повідомте нам, де живе DBA, щоб ми могли домовитись про коригування перкусії

Відредагуйте після оновлення 02 червня

4-й стовпець не є частиною некластеризованого індексу, тому він використовує кластерний індекс.

Спробуйте змінити індекс NC на ВКЛЮЧИТИ стовпець значення, щоб він не мав доступу до стовпця значення для кластерного індексу

create nonclustered index ix_hugetable on dbo.hugetable (
    fk asc, added asc
) include(value)

Примітка: Якщо значення не є нульовим, то воно те саме, що COUNT(*)семантично. Але для SUM воно потребує фактичного значення, а не існування .

В якості прикладу, якщо ви зміните COUNT(value)до COUNT(DISTINCT value) без зміни індексу слід розбити запит ще раз , тому що він повинен обробляти значення як значення, а не існування.

Запит потребує 3 стовпців: доданий, fk, значення. Перші 2 фільтруються / з’єднуються, тому є ключові стовпці. Значення просто використовується, тому може бути включено. Класичне використання індексу покриття.


Так, у мене було в голові, що кластеризовані та некластеризовані індекси мають fk & додані в іншому порядку. Я не можу повірити, що я цього не помітив, майже так само, як не можу повірити, що це налаштування таким чином. Я завтра змінитиму кластерний індекс, а потім пітиму на вулицю за кавою, поки вона відбудується.
Швидкий Джо Сміт

Я змінив індексацію і збився з FORCE ORDER, намагаючись зменшити кількість запитів на великій таблиці, але безрезультатно. Моє запитання оновлено.
Швидкий Джо Сміт

@Quick Joe Smith: оновив мою відповідь
gbn

Так, я спробував це недовго після цього. Оскільки перебудова індексу займає так багато часу, я забув про це і спочатку подумав, що прискорив це, роблячи щось зовсім не пов'язане.
Швидкий Джо Сміт

2

Визначте індекс hugetableна лише addedстовпці.

БД використовуватимуть індекс багаточастин (багато стовпців) лише в правій частині списку стовпців, оскільки значення зліва зліва. Ваш запит не вказується fkв пункті де першого запиту, тому він ігнорує індекс.


Показує план виконання, індекс (ix_hugetable) в даний час добився. Або ви кажете, що цей індекс не підходить для запиту?
Швидкий Джо Сміт

Індекс не підходить. Хто знає, як це "використання індексу". Досвід підказує мені, що це ваша проблема. Спробуйте і розкажіть, як це йде.
богем

@Quick Joe Smith - ви пробували пропозицію @ Bohemian? Що, де результати?
Лівен Кірсмейкер

2
Я не погоджуюсь: пункт ON вперше логічно обробляється і фактично є СУДО на практиці, тому OP повинен спробувати обидва стовпчики. Відсутня індексація на fk взагалі = кластерне сканування індексу або пошук ключів, щоб отримати значення fk для СПОЛУЧЕННЯ. Чи можете ви додати деякі посилання на описану вами поведінку, будь ласка? Спеціально для SQL Server, враховуючи, що у вас мало попередньої історії для відповіді на цю RDBMS. Насправді, -1 заднім числом, як я набрав цей коментар
gbn

2

План виконання вказує, що вкладений цикл використовується на #smalltable, і що сканування індексу над величезним таблицею виконується 480 разів (для кожного рядка в #smalltable).

Це очікування, яке я очікую від використання оптимізатора запитів, припускаючи, що цикл приєднається до правильного вибору. Альтернатива полягає в тому, щоб циклічно фіксувати 250 мільйонів разів і виконувати пошук у таблиці #temp кожен раз, що може зайняти години / дні.

Індекс, який ви змушуєте використовувати в об'єднанні MERGE, становить майже 250 млн. Рядків * "розмір кожного ряду" - не маленький, принаймні, пару ГБ. Судячи з sp_spaceusedвисновку "пару ГБ", це може бути досить заниженим - приєднання до MERGE вимагає просунути індекс, який буде дуже інтенсивним вводу / виводу.


Я розумію, що існує 3 типи алгоритмів з'єднання, і що об'єднання об'єднань має найкращу ефективність, коли обидва входи впорядковані предикатом приєднання. Правильно чи неправильно, це результат, який я намагаюся отримати.
Швидкий Джо Сміт

2
Але в цьому є більше, ніж у цьому. Якщо у #smalltable було велика кількість рядків, то об'єднання об'єднань може бути доречним. Якщо, як випливає з назви, він має невелику кількість рядків, то правильним вибором може стати з'єднання циклу. Уявіть, що у #smalltable було один-два рядки, і вони збігалися порівняно з кількома рядками з другої таблиці - важко було б виправдати об'єднання об'єднань тут.
Чи буде

Я подумав, що там є більше; Я просто не знав, що це може бути. Оптимізація бази даних - це не зовсім мій сильний костюм, як ви, напевно, вже здогадалися.
Швидкий Джо Сміт

@Quick Joe Smith - дякую за sp_spaceused. 75 ГБ індексу та 18 ГБ даних - чи ix_hugetable не єдиний індекс на столі?
Чи буде

1
+1 Буде. Наразі планувальник робить правильно. Проблема полягає у випадкових пошуку диска через те, що ваші таблиці кластеризовані.
Дені де Бернарді,

1

Ваш індекс невірний. Див. Індекси дос та донтів .

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

Спробуйте додати кластерний індекс на hugetable(added, fk). Це повинно змусити планувальник знайти відповідні рядки з величезної таблиці, а петля гніздування або злиття з'єднати їх з маленькою таблицею.


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