Posso refactoring questa query per farla funzionare in parallelo?


12

Ho una query che impiega circa 3 ore per essere eseguita sul nostro server e non sfrutta l'elaborazione parallela. (circa 1,15 milioni di record in dbo.Deidentified, 300 record in dbo.NamesMultiWord). Il server ha accesso a 8 core.

  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;

ed ReplaceMultiwordè una procedura definita come:

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

La chiamata a ReplaceMultiwordprevenire la formazione di un piano parallelo? C'è un modo per riscriverlo per consentire il parallelismo?

ReplaceMultiword funziona in ordine decrescente perché alcuni dei sostituti sono versioni brevi di altri e voglio che la partita più lunga abbia successo.

Ad esempio, ci potrebbe essere "George Washington University" e un altro da "Washington University". Se la partita "Washington University" fosse la prima, allora "George" sarebbe lasciato indietro.

piano di query

Tecnicamente posso usare CLR, non ho familiarità con il modo di farlo.


3
L'assegnazione delle variabili ha un comportamento definito solo per una singola riga. La SELECT @var = REPLACE ... ORDER BYcostruzione non è garantita per funzionare come previsto. Esempio di elemento Connect (vedere la risposta di Microsoft). Quindi, passare a SQLCLR ha l'ulteriore vantaggio di garantire risultati corretti, il che è sempre bello.
Paul White 9

Risposte:


11

L'UDF sta impedendo il parallelismo. Inoltre sta causando quella bobina.

È possibile utilizzare CLR e una regex compilata per eseguire la ricerca e sostituirla. Non blocca il parallelismo fintanto che sono presenti gli attributi richiesti e sarà probabilmente molto più veloce rispetto all'esecuzione di 300 REPLACEoperazioni TSQL per chiamata di funzione.

Il codice di esempio è sotto.

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; 

Questo dipende dall'esistenza di un UDF CLR come di seguito (ciò DataAccessKind.Nonedovrebbe significare che la bobina scompare così come è lì per la protezione di Halloween e non è necessaria in quanto non accede alla tabella di destinazione).

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

Ho appena confrontato questo. Utilizzando la stessa tabella e il contenuto per ciascuno, il CLR ha impiegato 3: 03.51 per elaborare le 1.174.731 righe e l'UDF ha impiegato 3: 16.21. Ha fatto risparmiare tempo. Nella mia lettura casuale, sembra che SQL Server detesti parallelizzare le query UPDATE.
rsjaffe,

@rsjaffe deludente. Avrei sperato in un risultato molto migliore di quello. Qual è la dimensione dei dati coinvolti? (Somma della lunghezza dei dati di tutte le colonne interessate)
Martin Smith

608 milioni di caratteri, 1.216 GB, il formato è NVARCHAR. Stavo pensando di aggiungere una whereclausola usando un test per la corrispondenza con la regex, poiché la maggior parte delle scritture non sono necessarie - la densità di "hit" dovrebbe essere bassa, ma le mie abilità in C # (sono un ragazzo C ++) no portami lì. Stavo pensando sulla falsariga di una procedura public static SqlBoolean CanReplaceMultiWord(SqlString inputString, SqlXml replacementSpec)che sarebbe tornata, return Regex.IsMatch(inputString.ToString()); ma ricevo errori su quella dichiarazione di ritorno, come `System.Text.RegularExpressions.Regex è un tipo ma viene usato come una variabile.
rsjaffe,

4

Conclusione : l'aggiunta di criteri alla WHEREclausola e la suddivisione della query in quattro query separate, una per ciascun campo ha consentito a SQL Server di fornire un piano parallelo e ha reso la query eseguita 4 volte più veloce rispetto a prima senza il test aggiuntivo nella WHEREclausola. Dividere le query in quattro senza il test non lo ha fatto. Né ha aggiunto il test senza dividere le query. L'ottimizzazione del test ha ridotto il tempo di esecuzione totale a 3 minuti (dalle 3 ore originali).

Il mio UDF originale ha impiegato 3 ore e 16 minuti per elaborare 1.174.731 righe, con 1.216 GB di dati nvarchar testati. Utilizzando il CLR fornito da Martin Smith nella sua risposta, il piano di esecuzione non era ancora parallelo e l'attività ha richiesto 3 ore e 5 minuti. CLR, piano di esecuzione non parallelo

Dopo aver letto che i WHEREcriteri potrebbero aiutare a spingere UPDATEin parallelo, ho fatto quanto segue. Ho aggiunto una funzione al modulo CLR per vedere se il campo aveva una corrispondenza con il regex:

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

e, in internal class ReplaceSpecification, ho aggiunto il codice per eseguire il test contro il regex

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

Se tutti i campi sono testati in una singola istruzione, il server SQL non parallelizza il lavoro

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

Tempo di esecuzione oltre 4 1/2 ore e ancora in esecuzione. Progetto esecutivo: Test aggiunto, singola istruzione

Tuttavia, se i campi sono separati in istruzioni separate, viene utilizzato un piano di lavoro parallelo e il mio utilizzo della CPU passa dal 12% con i piani seriali al 100% con i piani paralleli (8 core).

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;

Tempo di esecuzione 46 minuti. Le statistiche di riga hanno mostrato che circa lo 0,5% dei record presentava almeno una corrispondenza regex. Progetto esecutivo: inserisci qui la descrizione dell'immagine

Ora, la principale resistenza in tempo era la WHEREclausola. Ho quindi sostituito il test regex nella WHEREclausola con l' algoritmo Aho-Corasick implementato come CLR. Ciò ha ridotto il tempo totale a 3 minuti e 6 secondi.

Ciò ha richiesto le seguenti modifiche. Carica l'assemblaggio e le funzioni per l'algoritmo Aho-Corasick. Cambia la WHEREclausola in

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

E aggiungi quanto segue prima del primo UPDATE

DECLARE @ac NVARCHAR(32);
SET @ac = dbo.CreateAhoCorasick(
  (SELECT NAMES FROM dbo.NamesMultiWord FOR XML RAW, root('root')),
  'en-us:i'
);
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.