Хоча я погоджуюся з іншими коментаторами, що це обчислювально дорога проблема, я думаю, що є багато можливостей для вдосконалення шляхом налаштування 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.
- Довжина як імен, так і фраз досить важлива для нового підходу. Я припускаю, що погані фрази, як правило, досить короткі, оскільки це, можливо, відповідає реальним випадкам використання. Імена досить довші, ніж погані фрази, але передбачається, що вони не мають тисяч символів. Я думаю, що це справедливе припущення, і довші рядки імен також уповільнюватимуть ваш початковий підхід.
- Певна частина вдосконалення (але ніде не наближена до всього) пояснюється тим, що новий підхід може використовувати паралелізм ефективніше, ніж старий (який працює однонитковим). Я на чотирьохядерному ноутбуці, тому приємно мати підхід, який може використовувати ці ядра для використання.
Пов’язана публікація в блозі
Аарон Бертран більш детально досліджує цей тип рішення у своїй публікації в блозі Один із способів отримати індексний пошук провідних% макіяжів .