Puis-je refactoriser cette requête pour qu'elle s'exécute en parallèle?


12

J'ai une requête qui prend environ 3 heures pour s'exécuter sur notre serveur - et elle ne tire pas parti du traitement parallèle. (environ 1,15 million d'enregistrements en dbo.Deidentified, 300 enregistrements en dbo.NamesMultiWord). Le serveur a accès à 8 cœurs.

  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;

et ReplaceMultiwordest une procédure définie comme:

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

Faut-il ReplaceMultiwordempêcher la formation d'un plan parallèle? Existe-t-il un moyen de réécrire cela pour permettre le parallélisme?

ReplaceMultiword s'exécute dans l'ordre décroissant car certains des remplacements sont des versions courtes d'autres, et je veux que la correspondance la plus longue réussisse.

Par exemple, il peut y avoir «George Washington University» et un autre de «Washington University». Si le match «Washington University» était le premier, alors «George» serait laissé pour compte.

plan de requête

Techniquement, je peux utiliser CLR, je ne sais pas comment le faire.


3
L'affectation de variable n'a qu'un comportement défini pour une seule ligne. La SELECT @var = REPLACE ... ORDER BYconstruction n'est pas garantie de fonctionner comme prévu. Exemple d'élément Connect (voir la réponse de Microsoft). Ainsi, le passage à SQLCLR a l'avantage supplémentaire de garantir des résultats corrects, ce qui est toujours agréable.
Paul White 9

Réponses:


11

L'UDF empêche le parallélisme. Il est également à l'origine de cette bobine.

Vous pouvez utiliser CLR et une expression régulière compilée pour effectuer votre recherche et remplacer. Il ne bloque pas le parallélisme tant que les attributs requis sont présents et sera probablement beaucoup plus rapide que d'effectuer 300 REPLACEopérations TSQL par appel de fonction.

Un exemple de code est ci-dessous.

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; 

Cela dépend de l'existence d'un UDF CLR comme ci-dessous (cela DataAccessKind.Nonedevrait signifier que la bobine disparaît ainsi que celle qui est là pour la protection d'Halloween et n'est pas nécessaire car cela n'accède pas à la table cible).

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

Je viens de comparer cela. En utilisant la même table et le même contenu pour chacun, le CLR a pris 3: 03,51 pour traiter les 1 174 731 lignes, et l'UDF a pris 3: 16,21. Cela a fait gagner du temps. Dans ma lecture occasionnelle, il semble que SQL Server répugne à paralléliser les requêtes UPDATE.
rsjaffe

@rsjaffe décevant. J'aurais espéré un résultat bien meilleur que cela. Quelle est la taille des données impliquées? (Somme de la longueur des données de toutes les colonnes affectées)
Martin Smith

608 millions de caractères, 1,216 Go, le format est NVARCHAR. Je pensais ajouter une whereclause en utilisant un test de correspondance avec l'expression régulière, car la plupart des écritures ne sont pas nécessaires - la densité des `` hits '' devrait être faible, mais mes compétences en C # (je suis un gars C ++) ne l'ont pas amenez-moi là. Je pensais dans le sens d'une procédure public static SqlBoolean CanReplaceMultiWord(SqlString inputString, SqlXml replacementSpec)qui retournerait return Regex.IsMatch(inputString.ToString()); mais j'obtiens des erreurs sur cette déclaration de retour, comme `System.Text.RegularExpressions.Regex est un type mais est utilisé comme une variable.
rsjaffe

4

Bottom line : L'ajout de critères à la WHEREclause et la division de la requête en quatre requêtes distinctes, une pour chaque champ, a permis au serveur SQL de fournir un plan parallèle et a rendu la requête exécutée 4X aussi vite qu'elle l'avait fait sans le test supplémentaire de la WHEREclause. Diviser les requêtes en quatre sans le test n'a pas fait cela. Ni l'ajout du test sans fractionner les requêtes. L'optimisation du test a réduit le temps d'exécution total à 3 minutes (par rapport aux 3 heures d'origine).

Mon UDF d'origine a pris 3 heures 16 minutes pour traiter 1 174 731 lignes, avec 1 216 Go de données nvarchar testées. En utilisant le CLR fourni par Martin Smith dans sa réponse, le plan d'exécution n'était toujours pas parallèle et la tâche a pris 3 heures et 5 minutes. CLR, plan d'exécution non parallèle

Après avoir lu ces WHEREcritères pourrait aider à pousser UPDATEen parallèle, j'ai fait ce qui suit. J'ai ajouté une fonction au module CLR pour voir si le champ avait une correspondance avec l'expression régulière:

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

et, dans internal class ReplaceSpecification, j'ai ajouté le code pour exécuter le test contre l'expression régulière

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

Si tous les champs sont testés dans une seule instruction, SQL Server ne parallélise pas le travail

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

Temps d'exécution de plus de 4 1/2 heures et toujours en cours d'exécution. Plan d'exécution: Test ajouté, déclaration unique

Cependant, si les champs sont séparés en déclarations distinctes, un plan de travail parallèle est utilisé, et mon utilisation du processeur passe de 12% avec les plans série à 100% avec les plans parallèles (8 cœurs).

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;

Temps pour exécuter 46 minutes. Les statistiques sur les rangées ont montré qu'environ 0,5% des enregistrements avaient au moins une correspondance d'expression régulière. Plan d'exécution: entrez la description de l'image ici

Maintenant, le principal frein au temps était la WHEREclause. J'ai ensuite remplacé le test d'expression WHERErégulière dans la clause par l' algorithme Aho-Corasick implémenté en tant que CLR. Cela a réduit le temps total à 3 minutes 6 secondes.

Cela a nécessité les modifications suivantes. Chargez l'assemblage et les fonctions de l'algorithme Aho-Corasick. Remplacez la WHEREclause par

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

Et ajoutez ce qui suit avant le premier UPDATE

DECLARE @ac NVARCHAR(32);
SET @ac = dbo.CreateAhoCorasick(
  (SELECT NAMES FROM dbo.NamesMultiWord FOR XML RAW, root('root')),
  'en-us:i'
);
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.