Чи можу я відновити цей запит, щоб змусити його паралельно працювати?


12

У мене є запит, який триває близько 3 годин для запуску на нашому сервері - і це не використовує переваги паралельної обробки. (близько 1,15 млн записів dbo.Deidentified, 300 записів у dbo.NamesMultiWord). Сервер має доступ до 8 ядер.

  UPDATE dbo.Deidentified 
     WITH (TABLOCK)
  SET IndexedXml = dbo.ReplaceMultiWord(IndexedXml),
      DE461 = dbo.ReplaceMultiWord(DE461),
      DE87 = dbo.ReplaceMultiWord(DE87),
      DE15 = dbo.ReplaceMultiWord(DE15)
  WHERE InProcess = 1;

і ReplaceMultiwordце процедура, визначена як:

SELECT @body = REPLACE(@body,Names,Replacement)
 FROM dbo.NamesMultiWord
 ORDER BY [WordLength] DESC
RETURN @body --NVARCHAR(MAX)

Чи є закликом ReplaceMultiwordзапобігти формуванню паралельного плану? Чи є спосіб переписати це, щоб дозволити паралелізм?

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

Наприклад, може бути "Університет Джорджа Вашингтона" та ще один із "Університет Вашингтона". Якби матч "Університету Вашингтона" був першим, то "Джордж" залишився б позаду.

план запитів

Технічно я можу використовувати CLR, я просто не знайомий, як це зробити.


3
Змінне призначення має лише певну поведінку для одного рядка. SELECT @var = REPLACE ... ORDER BYКонструкція не гарантує роботу , як ви очікуєте. Приклад підключення елемента (див. Відповідь від Microsoft). Отже, перехід на SQLCLR має додаткову перевагу в гарантуванні правильних результатів, що завжди приємно.
Пол Білий 9

Відповіді:


11

АДС запобігає паралелізму. Це також викликає цю котушку.

Ви можете використовувати CLR та компільований регулярний вираз для пошуку та заміни. Він не блокує паралелізм до тих пір, поки потрібні атрибути є, і, ймовірно, будуть значно швидшими, ніж виконання 300 REPLACEоперацій TSQL за виклик функції.

Приклад коду наведено нижче.

DECLARE @X XML = 
(
    SELECT Names AS [@find],
           Replacement  AS [@replace]
    FROM  dbo.NamesMultiWord 
    ORDER BY [WordLength] DESC
    FOR XML PATH('x'), ROOT('spec')
);

UPDATE dbo.Deidentified WITH (TABLOCK)
SET    IndexedXml = dbo.ReplaceMultiWord(IndexedXml, @X),
       DE461 = dbo.ReplaceMultiWord(DE461, @X),
       DE87 = dbo.ReplaceMultiWord(DE87, @X),
       DE15 = dbo.ReplaceMultiWord(DE15, @X)
WHERE  InProcess = 1; 

Це залежить від існування CLR UDF, як показано нижче (це DataAccessKind.Noneмає означати, що котушка зникає, а також що є для захисту на Хеллоуїн і не потрібна, оскільки це не має доступу до цільової таблиці).

using System;
using System.Data.SqlTypes;
using Microsoft.SqlServer.Server;
using System.Text.RegularExpressions;
using System.Collections.Generic;
using System.Xml;

public partial class UserDefinedFunctions
{
    //TODO: Concurrency?
    private static readonly Dictionary<string, ReplaceSpecification> cachedSpecs = 
                        new Dictionary<string, ReplaceSpecification>();

    [SqlFunction(IsDeterministic = true,
                 IsPrecise = true,
                 DataAccess = DataAccessKind.None,
                 SystemDataAccess = SystemDataAccessKind.None)]
    public static SqlString ReplaceMultiWord(SqlString inputString, SqlXml replacementSpec)
    {
        //TODO: Implement something to drop things from the cache and use a shorter key.
        string s = replacementSpec.Value;
        ReplaceSpecification rs;

        if (!cachedSpecs.TryGetValue(s, out rs))
        {
            var doc = new XmlDocument();
            doc.LoadXml(s);
            rs = new ReplaceSpecification(doc);
            cachedSpecs[s] = rs;
        }

        string result = rs.GetResult(inputString.ToString());
        return new SqlString(result);
    }


    internal class ReplaceSpecification
    {
        internal ReplaceSpecification(XmlDocument doc)
        {
            Replacements = new Dictionary<string, string>();

            XmlElement root = doc.DocumentElement;
            XmlNodeList nodes = root.SelectNodes("x");

            string pattern = null;
            foreach (XmlNode node in nodes)
            {
                if (pattern != null)
                    pattern = pattern + "|";

                string find = node.Attributes["find"].Value.ToLowerInvariant();
                string replace = node.Attributes["replace"].Value;
                 //TODO: Escape any special characters in the regex syntax
                pattern = pattern + find;
                Replacements[find] = replace;
            }

            if (pattern != null)
            {
                pattern = "(?:" + pattern + ")";
                Regex = new Regex(pattern, RegexOptions.IgnoreCase | RegexOptions.Compiled);
            }


        }
        private Regex Regex { get; set; }

        private Dictionary<string, string> Replacements { get; set; }


        internal string GetResult(string inputString)
        {
            if (Regex == null)
                return inputString;

            return Regex.Replace(inputString,
                                 (Match m) =>
                                 {
                                     string s;
                                     if (Replacements.TryGetValue(m.Value.ToLowerInvariant(), out s))
                                     {
                                         return s;
                                     }
                                     else
                                     {
                                         throw new Exception("Missing replacement definition for " + m.Value);
                                     }
                                 });
        }
    }
}

Я щойно це порівняв. Використовуючи однакову таблицю та вміст для кожного, CLR знадобився 3: 03,51 для обробки 1,174,731 рядків, а UDF - 3: 16,21. Це заощадило час. У моєму випадковому читанні, схоже, SQL Server ненавидить паралелізувати UPDATE запити.
rsjaffe

@rsjaffe розчаровує. Я би сподівався на набагато кращий результат від цього. Який розмір залучених даних? (Сума довжини даних усіх постраждалих стовпців)
Мартін Сміт,

608 мільйонів символів, 1,221 ГБ, формат - NVARCHAR. Я думав додати whereпропозицію за допомогою тесту на відповідність з регулярним виразом, оскільки більшість записів непотрібні - щільність 'хітів' має бути низькою, але мої навички C # (я хлопець C ++) не заведи мене туди. Я роздумував над процедурою, public static SqlBoolean CanReplaceMultiWord(SqlString inputString, SqlXml replacementSpec)яка повертається, return Regex.IsMatch(inputString.ToString()); але я отримую помилки у цьому операторі return, наприклад, `System.Text.RegularExpressions.Regex - тип, але використовується як змінна.
rsjaffe

4

Підсумок : Додавання критеріїв до WHEREпункту та розділення запиту на чотири окремі запити, по одному для кожного поля дозволяв SQL-серверу надати паралельний план і змусив запит виконуватись 4X так швидко, як це було без додаткового тесту в WHEREпункті. Розбиття запитів на чотири без тесту не зробило цього. Також не додавали тест, не розбиваючи запити. Оптимізація тесту скоротила загальний час роботи до 3 хвилин (з початкових 3 годин).

Для мого оригінального UDF було потрібно 3 години 16 хвилин, щоб обробити 1174 731 рядок, протестовано 1,221 ГБ даних nvarchar. Використовуючи CLR, наданий у своїй відповіді Мартіном Смітом, план виконання ще не був паралельним, а завдання займало 3 години 5 хвилин. CLR, план виконання не паралельний

Прочитавши, що WHEREкритерії можуть допомогти просунути UPDATEпаралель, я зробив наступне. Я додав функцію до модуля CLR, щоб перевірити, чи відповідає поле збігу з регулярним виразом:

[SqlFunction(IsDeterministic = true,
         IsPrecise = true,
         DataAccess = DataAccessKind.None,
         SystemDataAccess = SystemDataAccessKind.None)]
public static SqlBoolean CanReplaceMultiWord(SqlString inputString, SqlXml replacementSpec)
{
    string s = replacementSpec.Value;
    ReplaceSpecification rs;
    if (!cachedSpecs.TryGetValue(s, out rs))
    {
        var doc = new XmlDocument();
        doc.LoadXml(s);
        rs = new ReplaceSpecification(doc);
        cachedSpecs[s] = rs;
    }
    return rs.IsMatch(inputString.ToString());
}

і, в internal class ReplaceSpecification, я додав код для виконання тесту проти регулярного вираження

    internal bool IsMatch(string inputString)
    {
        if (Regex == null)
            return false;
        return Regex.IsMatch(inputString);
    }

Якщо всі поля тестуються в одному операторі, SQL-сервер не паралелізує роботу

UPDATE dbo.DeidentifiedTest
SET IndexedXml = dbo.ReplaceMultiWord(IndexedXml, @X),
    DE461 = dbo.ReplaceMultiWord(DE461, @X),
    DE87 = dbo.ReplaceMultiWord(DE87, @X),
    DE15 = dbo.ReplaceMultiWord(DE15, @X)
WHERE InProcess = 1
    AND (dbo.CanReplaceMultiWord(IndexedXml, @X) = 1
    OR DE15 = dbo.ReplaceMultiWord(DE15, @X)
    OR dbo.CanReplaceMultiWord(DE87, @X) = 1
    OR dbo.CanReplaceMultiWord(DE15, @X) = 1);

Час на виконання понад 4 1/2 години та все ще працює. План виконання: Тест додано, одне твердження

Однак, якщо поля розділені на окремі оператори, використовується паралельний робочий план, і моє використання процесора переходить від 12% при послідовних планах до 100% при паралельних планах (8 ядер).

UPDATE dbo.DeidentifiedTest
SET IndexedXml = dbo.ReplaceMultiWord(IndexedXml, @X)
WHERE InProcess = 1
    AND dbo.CanReplaceMultiWord(IndexedXml, @X) = 1;

UPDATE dbo.DeidentifiedTest
SET DE461 = dbo.ReplaceMultiWord(DE461, @X)
WHERE InProcess = 1
    AND dbo.CanReplaceMultiWord(DE461, @X) = 1;

UPDATE dbo.DeidentifiedTest
SET DE87 = dbo.ReplaceMultiWord(DE87, @X)
WHERE InProcess = 1
    AND dbo.CanReplaceMultiWord(DE87, @X) = 1;

UPDATE dbo.DeidentifiedTest
SET DE15 = dbo.ReplaceMultiWord(DE15, @X)
WHERE InProcess = 1
    AND dbo.CanReplaceMultiWord(DE15, @X) = 1;

Час на виконання 46 хвилин. Рядкова статистика показала, що близько 0,5% записів мали принаймні один збіг регулярних виразів. План виконання: введіть тут опис зображення

Тепер основним затримкою часу був WHEREпункт. Потім я замінив тест регулярного вираження в WHEREпункті алгоритмом Aho-Corasick, реалізованим як CLR. Це зменшило загальний час до 3 хвилин 6 секунд.

Це вимагало наступних змін. Завантажте збірку та функції алгоритму Aho-Corasick. Змініть WHEREпункт на

WHERE  InProcess = 1 AND dbo.ContainsWordsByObject(ISNULL(FieldBeingTestedGoesHere,'x'), @ac) = 1; 

І додайте наступне перед першим UPDATE

DECLARE @ac NVARCHAR(32);
SET @ac = dbo.CreateAhoCorasick(
  (SELECT NAMES FROM dbo.NamesMultiWord FOR XML RAW, root('root')),
  'en-us:i'
);
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.