Kann ich diese Abfrage umgestalten, damit sie parallel ausgeführt wird?

12

Ich habe eine Abfrage, deren Ausführung auf unserem Server ungefähr 3 Stunden dauert - und die Parallelverarbeitung wird nicht ausgenutzt. (ungefähr 1,15 Millionen Datensätze in dbo.Deidentified, 300 Datensätze in dbo.NamesMultiWord). Der Server hat Zugriff auf 8 Kerne.

  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;

und ReplaceMultiwordist eine Prozedur definiert als:

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

Ist die Forderung, ReplaceMultiworddie Bildung eines Parallelplans zu verhindern? Gibt es eine Möglichkeit, dies umzuschreiben, um Parallelität zu ermöglichen?

ReplaceMultiword wird in absteigender Reihenfolge ausgeführt, da einige der Ersetzungen kurze Versionen anderer sind und ich möchte, dass die längste Übereinstimmung erfolgreich ist.

Zum Beispiel kann es "George Washington University" und eine andere von der "Washington University" geben. Wenn das Match der Washington University das erste wäre, würde George zurückbleiben.

Abfrageplan

Technisch kann ich CLR verwenden, ich bin nur nicht vertraut damit.

rsjaffe
quelle
3
Die Variablenzuweisung hat nur für eine einzelne Zeile ein definiertes Verhalten. Es SELECT @var = REPLACE ... ORDER BYwird nicht garantiert, dass die Konstruktion so funktioniert, wie Sie es erwarten. Beispiel für ein Verbindungselement (siehe Antwort von Microsoft). Die Umstellung auf SQLCLR hat den zusätzlichen Vorteil, dass korrekte Ergebnisse garantiert werden. Das ist immer schön.
Paul White 9

Antworten:

11

Die UDF verhindert Parallelität. Es verursacht auch diese Spule.

Sie können CLR und einen kompilierten regulären Ausdruck verwenden, um zu suchen und zu ersetzen. Sie blockiert die Parallelität nicht, solange die erforderlichen Attribute vorhanden sind, und ist wahrscheinlich wesentlich schneller als die Ausführung von 300 TSQLREPLACE Operationen pro Funktionsaufruf.

Beispielcode ist unten.

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; 

Dies hängt von der Existenz einer CLR-UDF wie DataAccessKind.Nonefolgt ab (dies sollte bedeuten, dass die Spule nicht mehr angezeigt wird, da sie für den Halloween-Schutz vorgesehen ist und nicht benötigt wird, da sie nicht auf die Zieltabelle zugreift).

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
quelle
Ich habe das gerade verglichen. Bei Verwendung der gleichen Tabelle und des gleichen Inhalts für die Verarbeitung der 1.174.731 Zeilen benötigte die CLR 3: 03.51 und die UDF 3: 16.21. Das hat Zeit gespart. In meiner gelegentlichen Lektüre sieht es so aus, als ob SQL Server es ablehnt, UPDATE-Abfragen zu parallelisieren.
Rsjaffe
@ Rsjaffe enttäuschend. Ich hätte mir ein viel besseres Ergebnis erhofft. Wie groß sind die Daten? (Summe der Datenlänge aller betroffenen Spalten)
Martin Smith
608 Millionen Zeichen, 1.216 GB, das Format ist NVARCHAR. Ich habe darüber nachgedacht, eine whereKlausel hinzuzufügen , die einen Test für die Übereinstimmung mit dem regulären Ausdruck verwendet, da die meisten Schreibvorgänge unnötig sind - die Dichte der Treffer sollte niedrig sein, meine C # -Fähigkeiten (ich bin ein C ++ - Typ) jedoch nicht Bring mich hin. Ich dachte an eine Prozedur public static SqlBoolean CanReplaceMultiWord(SqlString inputString, SqlXml replacementSpec), die zurückgeben würde, return Regex.IsMatch(inputString.ToString()); aber ich erhalte Fehler in dieser return-Anweisung, wie `System.Text.RegularExpressions.Regex ist ein Typ, der aber wie eine Variable verwendet wird.
Rsjaffe
4

Fazit : Durch Hinzufügen von Kriterien zur WHEREKlausel und Aufteilen der Abfrage in vier separate Abfragen konnte SQL Server mit einem für jedes Feld einen parallelen Plan erstellen und die Abfrage ohne den zusätzlichen Test in der WHEREKlausel 4-mal so schnell ausführen wie zuvor . Das Aufteilen der Abfragen in vier ohne den Test hat das nicht getan. Der Test wurde auch nicht hinzugefügt, ohne die Abfragen aufzuteilen. Durch die Optimierung des Tests wurde die Gesamtlaufzeit auf 3 Minuten reduziert (von den ursprünglichen 3 Stunden).

Mein ursprünglicher UDF benötigte 3 Stunden und 16 Minuten, um 1.174.731 Zeilen zu verarbeiten, wobei 1.216 GB nvarchar-Daten getestet wurden. Unter Verwendung der von Martin Smith in seiner Antwort angegebenen CLR war der Ausführungsplan immer noch nicht parallel und die Aufgabe dauerte 3 Stunden und 5 Minuten. CLR, Ausführungsplan nicht parallel

Nachdem ich gelesen hatte, dass WHEREKriterien helfen könnten, eine UPDATEParallele zu schaffen, tat ich Folgendes. Ich habe dem CLR-Modul eine Funktion hinzugefügt, um festzustellen, ob das Feld mit dem regulären Ausdruck übereinstimmt:

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

und in habe internal class ReplaceSpecificationich den Code hinzugefügt, um den Test gegen den regulären Ausdruck auszuführen

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

Wenn alle Felder in einer einzigen Anweisung getestet werden, wird die Arbeit von SQL Server nicht parallelisiert

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

Ausführungszeit über 4 1/2 Stunden und läuft noch. Ausführungsplan: Test hinzugefügt, einzelne Anweisung

Wenn die Felder jedoch in separate Anweisungen unterteilt sind, wird ein paralleler Arbeitsplan verwendet, und meine CPU-Auslastung reicht von 12% bei seriellen Plänen bis 100% bei parallelen Plänen (8 Kerne).

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;

Ausführungszeit 46 Minuten. Die Zeilenstatistik ergab, dass etwa 0,5% der Datensätze mindestens eine Regex-Übereinstimmung aufwiesen. Ausführungsplan: Bildbeschreibung hier eingeben

Jetzt war der wichtigste Nachteil die WHEREKlausel. Ich habe dann den Regex-Test in der WHEREKlausel durch den Aho-Corasick-Algorithmus ersetzt als CLR implementierten . Dadurch wurde die Gesamtzeit auf 3 Minuten und 6 Sekunden reduziert.

Dies erforderte die folgenden Änderungen. Laden Sie die Assembly und Funktionen für den Aho-Corasick-Algorithmus. Ändern Sie die WHEREKlausel in

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

Fügen Sie vor dem ersten Folgendes hinzu UPDATE

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