Могу ли я провести рефакторинг этого запроса, чтобы он работал параллельно?

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, я просто не знаю, как это сделать.

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

Ответы:

11

UDF предотвращает параллелизм. Это также вызывает эту катушку.

Вы можете использовать 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; 

Это зависит от наличия UDF CLR, как показано ниже (это 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 разочаровывает. Я бы надеялся на гораздо лучший результат, чем этот. Каков размер данных? (Сумма длины данных всех затронутых столбцов)
Martin Smith
608 миллионов символов, 1,216 ГБ, формат NVARCHAR. Я думал о добавлении whereпредложения, используя тест на совпадение с регулярным выражением, так как большинство записей не нужны - плотность «попаданий» должна быть низкой, но мои навыки C # (я парень C ++) не доставь меня туда. Я думал о процедуре public static SqlBoolean CanReplaceMultiWord(SqlString inputString, SqlXml replacementSpec), которая возвращала бы, return Regex.IsMatch(inputString.ToString()); но я получаю ошибки в этом операторе возврата, например, `System.Text.RegularExpressions.Regex является типом, но используется как переменная.
rsjaffe
4

Итог : добавление критериев к WHEREпредложению и разделение запроса на четыре отдельных запроса, по одному для каждого поля, позволило SQL-серверу предоставить параллельный план и ускорить выполнение запроса в 4 раза, как это было без дополнительного теста в WHEREпредложении. Разделение запросов на четыре без теста не сделало этого. Ни один не сделал добавление теста без разделения запросов. Оптимизация теста сократила общее время выполнения до 3 минут (с первоначальных 3 часов).

Моему исходному UDF потребовалось 3 часа 16 минут, чтобы обработать 1 174 731 строку, при этом было протестировано 1,216 ГБ данных 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,5 часов и все еще работает. План выполнения: Тест добавлен, одно утверждение

Однако, если поля разделены на отдельные операторы, используется параллельный рабочий план, и моя загрузка ЦП увеличивается с 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'
);
rsjaffe
источник