Повільне оновлення на великій таблиці із запитом


16

З SourceTableзаписом> 15MM записів та записом Bad_Phrase> 3K, наступний запит потребує майже 10 годин для запуску на SQL Server 2005 SP4.

UPDATE [SourceTable] 
SET 
    Bad_Count=
             (
               SELECT 
                  COUNT(*) 
               FROM Bad_Phrase 
               WHERE 
                  [SourceTable].Name like '%'+Bad_Phrase.PHRASE+'%'
             )

Англійською мовою цей запит підраховує кількість відмінних фраз, перелічених у Bad_Phrase, які є підрядком поля Nameв, SourceTableа потім розміщують цей результат у полі Bad_Count.

Я хотів би отримати кілька пропозицій щодо того, як зробити цей запит значно швидшим.


3
Таким чином, ви скануєте таблицю 3K разів і потенційно оновлюєте всі 15MM рядки всі 3K рази, і ви очікуєте, що вона буде швидкою?
Аарон Бертран

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

3
Метью, ти розглядав повну індексацію тексту? Ви можете використовувати такі речі, як CONTAINS і все одно отримувати перевагу від індексації для цього пошуку.
swasheck

У цьому випадку я б запропонував спробувати логіку на основі рядків (тобто замість 1 оновлення рядків 15ММ, 15MM оновлює кожен рядок у SourceTable, або оновлює деякі відносно невеликі шматки). Загальний час не буде швидшим (навіть якщо це можливо в даному конкретному випадку), але такий підхід дозволяє решті системи продовжувати працювати без будь-яких перерв, дає вам контроль над розміром журналу транзакцій (скажімо, здійснюйте кожні 10k оновлення), переривайте оновити в будь-який час, не втрачаючи всіх попередніх оновлень ...
a1ex07

2
@swasheck Повний текст - це гарна ідея (це нове в 2005 році, я вважаю, тому це може бути застосовано і тут), але не можна було б забезпечити той самий функціонал, який запитував плакат, оскільки повнотекстові вказівки слів, а не довільні підрядки. Сказав інший спосіб, повнотекстовий текст не знайде відповідності для "мурашника" в межах слова "фантастичний". Але можливо, що бізнес-вимоги можуть бути змінені таким чином, щоб повноцінний текст став застосовним.
Джефф Паттерсон

Відповіді:


21

Хоча я погоджуюся з іншими коментаторами, що це обчислювально дорога проблема, я думаю, що є багато можливостей для вдосконалення шляхом налаштування SQL, який ви використовуєте. Для ілюстрації я створив підроблений набір даних з іменами 15MM та 3K фраз, застосував старий підхід та застосував новий підхід.

Повний сценарій для створення підробленого набору даних та випробування нового підходу

TL; DR

На моїй машині та в цьому підробленому наборі даних оригінальний підхід займає близько 4 годин . Запропонований новий підхід займає близько 10 хвилин , що значно покращиться. Ось короткий підсумок запропонованого підходу:

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


Оригінальний підхід: алгоритмічний аналіз

З плану оригінального UPDATEвисловлювання ми бачимо, що обсяг роботи лінійно пропорційний як кількості імен (15ММ), так і кількості фраз (3К). Отже, якщо ми помножимо кількість імен і фраз на 10, загальний час виконання буде приблизно в 100 разів повільніше.

Запит фактично пропорційний довжині nameа також; хоча це дещо приховано в плані запитів, воно відбувається через "кількість виконань" для пошуку в котушку таблиці. У фактичному плані ми бачимо, що це відбувається не один раз на кожне name, а фактично один раз за зміщення символу в межах name. Таким чином, цей підхід є O ( # names* # phrases* name length) у складності виконання.

введіть тут опис зображення


Новий підхід: код

Цей код також доступний у повному пастебі, але я скопіював його сюди для зручності. Пастебін також має повне визначення процедури, яке включає @minIdі @maxIdзмінні, які ви бачите нижче, щоб визначити межі поточної партії.

-- For each name, generate the string at each offset
DECLARE @maxBadPhraseLen INT = (SELECT MAX(LEN(phrase)) FROM Bad_Phrase)
SELECT s.id, sub.sub_name
INTO #SubNames
FROM (SELECT * FROM SourceTable WHERE id BETWEEN @minId AND @maxId) s
CROSS APPLY (
    -- Create a row for each substring of the name, starting at each character
    -- offset within that string.  For example, if the name is "abcd", this CROSS APPLY
    -- will generate 4 rows, with values ("abcd"), ("bcd"), ("cd"), and ("d"). In order
    -- for the name to be LIKE the bad phrase, the bad phrase must match the leading X
    -- characters (where X is the length of the bad phrase) of at least one of these
    -- substrings. This can be efficiently computed after indexing the substrings.
    -- As an optimization, we only store @maxBadPhraseLen characters rather than
    -- storing the full remainder of the name from each offset; all other characters are
    -- simply extra space that isn't needed to determine whether a bad phrase matches.
    SELECT TOP(LEN(s.name)) SUBSTRING(s.name, n.n, @maxBadPhraseLen) AS sub_name 
    FROM Numbers n
    ORDER BY n.n
) sub
-- Create an index so that bad phrases can be quickly compared for a match
CREATE CLUSTERED INDEX IX_SubNames ON #SubNames (sub_name)

-- For each name, compute the number of distinct bad phrases that match
-- By "match", we mean that the a substring starting from one or more 
-- character offsets of the overall name starts with the bad phrase
SELECT s.id, COUNT(DISTINCT b.phrase) AS bad_count
INTO #tempBadCounts
FROM dbo.Bad_Phrase b
JOIN #SubNames s
    ON s.sub_name LIKE b.phrase + '%'
GROUP BY s.id

-- Perform the actual update into a "bad_count_new" field
-- For validation, we'll compare bad_count_new with the originally computed bad_count
UPDATE s
SET s.bad_count_new = COALESCE(b.bad_count, 0)
FROM dbo.SourceTable s
LEFT JOIN #tempBadCounts b
    ON b.id = s.id
WHERE s.id BETWEEN @minId AND @maxId


Новий підхід: плани запитів

Спочатку ми генеруємо підрядку, починаючи з кожного зміщення символів

введіть тут опис зображення

Потім створіть кластерний індекс у цих підрядках

введіть тут опис зображення

Тепер для кожної поганої фрази ми шукаємо в цих підрядках, щоб визначити будь-які збіги. Потім ми обчислюємо кількість відмінних фразових фраз, що відповідає одній або більше підрядках цього рядка. Це дійсно ключовий крок; через те, що ми індексували підрядки, нам більше не доведеться перевіряти повний перехресний добуток поганих фраз та імен. Цей крок, який робить фактичні обчислення, становить лише близько 10% від фактичного часу виконання (решта - це попередня обробка підрядів).

введіть тут опис зображення

Нарешті, виконайте фактичний оператор оновлення, використовуючи a, LEFT OUTER JOINщоб присвоїти кількість 0 будь-яким іменам, для яких ми не знайшли поганих фраз.

введіть тут опис зображення


Новий підхід: алгоритмічний аналіз

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

  • N = # імен
  • B = # поганих фраз
  • L = середня довжина імені, у символах

Етап попередньої обробки полягає O(N*L * LOG(N*L))в тому, щоб створити N*Lпідрядки та потім сортувати їх.

Фактична відповідність полягає O(B * LOG(N*L))в тому, щоб шукати підрядки для кожної поганої фрази.

Таким чином, ми створили алгоритм, який не лінійно масштабує кількість поганих фраз, ключове розблокування продуктивності, коли ми масштабуємо до 3К фраз і далі. Інший спосіб сказав, що оригінальна реалізація займає приблизно 10 разів, поки ми переходимо від 300 поганих фраз до 3К поганих фраз. Так само знадобиться ще 10 разів, якби ми перейшли від 3K поганих фраз до 30К. Нова реалізація, однак, збільшуватиме сублінійно і фактично займає менше, ніж у 2 рази часу, виміряного на 3К поганих фразах, коли масштабується до 30К поганих фраз.


Припущення / застереження

  • Я ділю загальну роботу на невеликі партії. Це, мабуть, хороша ідея для будь-якого підходу, але для нового підходу це особливо важливо, щоб SORTна підрядках було незалежним для кожної партії і легко вміщувалося в пам'яті. Ви можете маніпулювати розміром партії у міру необхідності, але не було б розумно спробувати всі рядки 15 ММ в одній партії.
  • Я на SQL 2014, а не на SQL 2005, оскільки я не маю доступу до машини SQL 2005. Я насторожився, щоб не використовувати жодного синтаксису, який недоступний у SQL 2005, але, можливо, я все-таки отримаю користь від функції тимчасового запису tempdb у SQL 2012+ та паралельної функції SELECT INTO у SQL 2014.
  • Довжина як імен, так і фраз досить важлива для нового підходу. Я припускаю, що погані фрази, як правило, досить короткі, оскільки це, можливо, відповідає реальним випадкам використання. Імена досить довші, ніж погані фрази, але передбачається, що вони не мають тисяч символів. Я думаю, що це справедливе припущення, і довші рядки імен також уповільнюватимуть ваш початковий підхід.
  • Певна частина вдосконалення (але ніде не наближена до всього) пояснюється тим, що новий підхід може використовувати паралелізм ефективніше, ніж старий (який працює однонитковим). Я на чотирьохядерному ноутбуці, тому приємно мати підхід, який може використовувати ці ядра для використання.


Пов’язана публікація в блозі

Аарон Бертран більш детально досліджує цей тип рішення у своїй публікації в блозі Один із способів отримати індексний пошук провідних% макіяжів .


6

Давайте відкладемо очевидну проблему, яку підняв Аарон Бертран в коментарях на секунду:

Отже, ви скануєте таблицю 3K разів і потенційно оновлюєте всі 15MM рядки всі 3K рази, і ви очікуєте, що вона буде швидкою?

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

Це означає, що SQL Server повинен зчитувати кожен рядок із таблиці продуктів, перевіряти, чи є він у будь-якій точці назви, а потім повертати наші результати.

Замініть слово "горіх" на кожне "погане слово" та "Продукт" на SourceTable, а потім поєднайте це з коментарем Аарона, і ви повинні почати розбиратися, чому це надзвичайно важко (читати неможливо), щоб змусити його швидко працювати за допомогою вашого поточного алгоритму.

Я бачу кілька варіантів:

  1. Переконайте бізнес придбати сервер монстра, який має стільки потужності, що він долає запит шляхом зсувної грубої сили. (Це не відбудеться, тому схрестіть пальці, інші варіанти краще)
  2. Використовуючи існуючий алгоритм, прийміть біль один раз, а потім розкладіть його. Це передбачає обчислення поганих слів на вставці, які сповільнюватимуть вставлення, і оновлять всю таблицю лише тоді, коли буде введено / виявлено нове погане слово.
  3. Отримати відповідь Джеффа . Це чудовий алгоритм і набагато кращий за все, що я б придумав.
  4. Зробіть варіант 2, але замініть свій алгоритм на Geoff's.

Залежно від ваших вимог я рекомендував би варіант 3 або 4.


0

По-перше, це лише дивне оновлення

Update [SourceTable]  
   Set [SourceTable].[Bad_Count] = [fix].[count]
  from [SourceTable] 
  join ( Select count(*) 
           from [Bad_Phrase]  
          where [SourceTable].Name like '%' + [Bad_Phrase].[PHRASE] + '%')

Як і "%" + [Bad_Phrase]. [PHRASE] вбиває тебе,
який не може використовувати індекс

Дизайн даних не є оптимальним для швидкості
Чи можете ви розділити [Bad_Phrase]. [ФРАЗ] на одну фразу / слово?
Якщо однаково словосполучення / слово з’являється більше одного, ви можете ввести його не раз, якщо ви хочете, щоб він мав більший підрахунок.
Отже, кількість рядків із поганою фазою збільшиться.
Якщо ви зможете, це стане набагато швидше

Update [SourceTable]  
   Set [SourceTable].[Bad_Count] = [fix].[count]
  from [SourceTable] 
  join ( select [PHRASE], count(*) as count 
           from [Bad_Phrase] 
          group by [PHRASE] 
       ) as [fix]
    on [fix].[PHRASE] = [SourceTable].[name]  
 where [SourceTable].[Bad_Count] <> [fix].[count]

Не впевнений, чи підтримує це 2005 р., Але повний текстовий індекс та використання вмісту містить


1
Я не думаю, що ОП хоче рахувати випадки поганого слова в таблиці поганих слів. Я думаю, що вони хочуть підрахувати кількість поганих слів, прихованих у вихідній таблиці. Наприклад, оригінальний код, ймовірно, дасть рахунок 2 для імені "shitass", але ваш код дасть рахунок 0.
Ерік,

1
@Erik "Ви можете розбити [Bad_Phrase]. [ФРАЗ] на одну фразу?" Невже ви не думаєте, що дизайн даних може бути виправленням? Якщо мета - знайти погані речі, то «eriK» з кількістю однієї або декількох достатньо.
папараццо
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.