Czy mogę zmienić to zapytanie, aby działało równolegle?

12

Mam zapytanie, które działa na naszym serwerze około 3 godzin - i nie korzysta z przetwarzania równoległego. (około 1,15 miliona rekordów dbo.Deidentified, 300 rekordów w dbo.NamesMultiWord). Serwer ma dostęp do 8 rdzeni.

  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;

i ReplaceMultiwordjest procedurą zdefiniowaną jako:

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

Czy wezwanie do ReplaceMultiwordzapobiegania tworzeniu równoległego planu? Czy istnieje sposób na przepisanie tego w celu umożliwienia równoległości?

ReplaceMultiword działa w kolejności malejącej, ponieważ niektóre zamienniki są krótkimi wersjami innych i chcę, aby jak najdłuższe dopasowanie zakończyło się powodzeniem.

Na przykład może istnieć „George Washington University” i inny z „Washington University”. Gdyby mecz „Washington University” był pierwszy, to „George” zostałby w tyle.

plan zapytań

Technicznie mogę korzystać z CLR, po prostu nie wiem, jak to zrobić.

rsjaffe
źródło
3
Przypisanie zmiennej ma zdefiniowane zachowanie tylko dla jednego wiersza. SELECT @var = REPLACE ... ORDER BYBudowa nie jest gwarantowane do pracy, jak można się spodziewać. Przykładowy element Connect (patrz odpowiedź Microsoft). Zatem przejście na SQLCLR ma tę dodatkową zaletę, że gwarantuje prawidłowe wyniki, co zawsze jest miłe.
Paul White 9

Odpowiedzi:

11

UDF zapobiega równoległości. To także powoduje, że ta szpula jest.

Możesz użyć CLR i skompilowanego wyrażenia regularnego do wyszukiwania i zamiany. Nie blokuje równoległości, dopóki obecne są wymagane atrybuty, i prawdopodobnie będzie znacznie szybsze niż wykonywanie 300 REPLACEoperacji TSQL na wywołanie funkcji.

Przykładowy kod znajduje się poniżej.

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; 

Zależy to od istnienia CLR UDF, jak poniżej ( DataAccessKind.Nonepowinno to oznaczać, że szpula znika, a także jest dostępna do ochrony na Halloween i nie jest potrzebna, ponieważ nie ma dostępu do tabeli docelowej).

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);
                                     }
                                 });
        }
    }
}
Martin Smith
źródło
Właśnie to sprawdziłem. Używając tej samej tabeli i zawartości dla każdego, CLR potrzebował 3: 03.51 do przetworzenia 1 174,731 wierszy, a UDF zajął 3: 16,21. Oszczędzało to czas. W moim swobodnym czytaniu wygląda na to, że SQL Server nie chce równolegle zapytań UPDATE.
rsjaffe
@rsjaffe rozczarowujące. Miałbym nadzieję na znacznie lepszy wynik. Jaki jest rozmiar danych? (Suma długości danych wszystkich dotkniętych kolumn)
Martin Smith
608 milionów znaków, 1,216 GB, format to NVARCHAR. Myślałem o dodaniu whereklauzuli za pomocą testu dopasowania do wyrażenia regularnego, ponieważ większość zapisów jest niepotrzebna - gęstość trafień powinna być niska, ale moje umiejętności w C # (jestem facetem w C ++) nie zabierz mnie tam. Myślałem zgodnie z procedurą, public static SqlBoolean CanReplaceMultiWord(SqlString inputString, SqlXml replacementSpec)która by zwróciła, return Regex.IsMatch(inputString.ToString()); ale otrzymuję błędy w tej instrukcji return, takie jak `System.Text.RegularExpressions.Regex jest typem, ale jest używany jak zmienna.
rsjaffe
4

Konkluzja : Dodanie kryteriów do WHEREklauzuli i podzielenie zapytania na cztery osobne zapytania, po jednym dla każdego pola, pozwoliło serwerowi SQL na zapewnienie równoległego planu i spowodowało, że zapytanie uruchomiono 4 razy szybciej niż bez dodatkowego testu w WHEREklauzuli. Podział zapytań na cztery bez testu tego nie zrobił. Nie dodano też testu bez dzielenia zapytań. Optymalizacja testu skróciła całkowity czas pracy do 3 minut (z pierwotnych 3 godzin).

Moja oryginalna funkcja UDF zajęła 3 godziny 16 minut na przetworzenie 1 177 731 wierszy i przetestowanie 1,216 GB danych nvarchar. Korzystając z CLR dostarczonego przez Martina Smitha w swojej odpowiedzi, plan wykonania wciąż nie był równoległy, a zadanie trwało 3 godziny i 5 minut. CLR, plan wykonania nie równoległy

Po zapoznaniu się z tymi WHEREkryteriami może pomóc w doprowadzeniu UPDATEdo równoległości, zrobiłem następujące. Dodałem funkcję do modułu CLR, aby sprawdzić, czy pole ma dopasowanie do wyrażenia regularnego:

[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());
}

i, w internal class ReplaceSpecification, dodałem kod, aby wykonać test względem wyrażenia regularnego

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

Jeśli wszystkie pola są testowane w jednej instrukcji, serwer SQL nie wykonuje pracy równoległej

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);

Czas na wykonanie ponad 4 1/2 godziny i nadal działa. Plan wykonania: Dodano test, jedna instrukcja

Jeśli jednak pola zostaną podzielone na osobne instrukcje, zostanie użyty równoległy plan pracy, a moje użycie procesora wzrośnie z 12% w przypadku planów szeregowych do 100% w przypadku planów równoległych (8 rdzeni).

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;

Czas na wykonanie 46 minut. Statystyki wierszy wykazały, że około 0,5% rekordów zawierało co najmniej jedno dopasowanie wyrażenia regularnego. Plan wykonania: wprowadź opis zdjęcia tutaj

Teraz głównym problemem na czasie była WHEREklauzula. Następnie zastąpiłem test wyrażenia regularnego w WHEREklauzuli algorytmem Aho-Corasick zaimplementowanym jako CLR. Skróciło to całkowity czas do 3 minut 6 sekund.

Wymagało to następujących zmian. Załaduj zespół i funkcje dla algorytmu Aho-Corasick. Zmień WHEREklauzulę na

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

I dodaj następujące przed pierwszym UPDATE

DECLARE @ac NVARCHAR(32);
SET @ac = dbo.CreateAhoCorasick(
  (SELECT NAMES FROM dbo.NamesMultiWord FOR XML RAW, root('root')),
  'en-us:i'
);
rsjaffe
źródło