¿Puedo refactorizar esta consulta para que se ejecute en paralelo?

12

Tengo una consulta que demora aproximadamente 3 horas en ejecutarse en nuestro servidor, y no aprovecha el procesamiento paralelo. (alrededor de 1,15 millones de registros dbo.Deidentified, 300 registros dbo.NamesMultiWord). El servidor tiene acceso a 8 núcleos.

  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;

y ReplaceMultiwordes un procedimiento definido como:

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

¿Es el llamado a ReplaceMultiwordprevenir la formación de un plan paralelo? ¿Hay alguna manera de reescribir esto para permitir el paralelismo?

ReplaceMultiword se ejecuta en orden descendente porque algunos de los reemplazos son versiones cortas de otros, y quiero que la coincidencia más larga tenga éxito.

Por ejemplo, puede haber 'George Washington University' y otra de 'Washington University'. Si el partido 'Washington University' fuera el primero, entonces 'George' se quedaría atrás.

plan de consulta

Técnicamente puedo usar CLR, simplemente no estoy familiarizado con cómo hacerlo.

rsjaffe
fuente
3
La asignación de variables solo tiene un comportamiento definido para una sola fila. No SELECT @var = REPLACE ... ORDER BYse garantiza que la construcción funcione como espera. Ejemplo de elemento de conexión (consulte la respuesta de Microsoft). Por lo tanto, cambiar a SQLCLR tiene la ventaja adicional de garantizar resultados correctos, lo que siempre es bueno.
Paul White 9

Respuestas:

11

El UDF está evitando el paralelismo. También está causando ese carrete.

Puede usar CLR y una expresión regular compilada para hacer su búsqueda y reemplazar. No bloquea el paralelismo mientras los atributos requeridos estén presentes y probablemente sea significativamente más rápido que realizar 300 REPLACEoperaciones TSQL por llamada a la función.

El código de ejemplo está abajo.

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; 

Esto depende de la existencia de un CLR UDF como se muestra a continuación (esto DataAccessKind.Nonedebería significar que el carrete desaparece y que está ahí para la protección de Halloween y no es necesario ya que esto no accede a la tabla de destino).

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
fuente
Acabo de comparar esto. Usando la misma tabla y contenido para cada uno, el CLR tomó 3: 03.51 para procesar las 1.174.731 filas, y el UDF tomó 3: 16.21. Ahorró tiempo. En mi lectura informal, parece que SQL Server detesta paralelizar las consultas de ACTUALIZACIÓN.
rsjaffe
@rsjaffe decepcionante. Hubiera esperado un resultado mucho mejor que eso. ¿Cuál es el tamaño de los datos involucrados? (Suma de la longitud de datos de todas las columnas afectadas)
Martin Smith
608 millones de caracteres, 1.216 GB, el formato es NVARCHAR. Estaba pensando en agregar una wherecláusula usando una prueba de coincidencia con la expresión regular, ya que la mayoría de las escrituras son innecesarias: la densidad de 'hits' debería ser baja, pero mis habilidades de C # (soy un chico de C ++) no lo hicieron llévame allí. Estaba pensando en las líneas de un procedimiento public static SqlBoolean CanReplaceMultiWord(SqlString inputString, SqlXml replacementSpec)que regresaría return Regex.IsMatch(inputString.ToString()); pero recibo errores en esa declaración de retorno, como `System.Text.RegularExpressions.Regex es un tipo pero se usa como una variable.
rsjaffe
4

En pocas palabras : al agregar criterios a la WHEREcláusula y dividir la consulta en cuatro consultas separadas, una para cada campo permitió que el servidor SQL proporcionara un plan paralelo e hizo que la consulta se ejecutara 4 veces más rápido que antes sin la prueba adicional en la WHEREcláusula. Dividir las consultas en cuatro sin la prueba no hizo eso. Tampoco agregar la prueba sin dividir las consultas. La optimización de la prueba redujo el tiempo total de ejecución a 3 minutos (desde las 3 horas originales).

Mi UDF original tardó 3 horas y 16 minutos en procesar 1.174.731 filas, con 1.216 GB de datos de nvarchar probados. Utilizando el CLR proporcionado por Martin Smith en su respuesta, el plan de ejecución aún no era paralelo y la tarea tomó 3 horas y 5 minutos. CLR, plan de ejecución no paralelo

Después de leer ese WHEREcriterio podría ayudar a empujar a un UPDATEparalelo, hice lo siguiente. Agregué una función al módulo CLR para ver si el campo tenía una coincidencia con la expresión regular:

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

y, en internal class ReplaceSpecification, agregué el código para ejecutar la prueba contra la expresión regular

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

Si todos los campos se prueban en una sola declaración, el servidor SQL no paraleliza el trabajo

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

Tiempo para ejecutar más de 4 1/2 horas y aún en ejecución. Plan de ejecución: Prueba agregada, declaración única

Sin embargo, si los campos se separan en declaraciones separadas, se usa un plan de trabajo paralelo y mi uso de CPU pasa del 12% con los planes en serie al 100% con los planes paralelos (8 núcleos).

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;

Tiempo para ejecutar 46 minutos. Las estadísticas de fila mostraron que aproximadamente el 0.5% de los registros tenían al menos una coincidencia de expresiones regulares. Plan de ejecución: ingrese la descripción de la imagen aquí

Ahora, el lastre principal en el tiempo era la WHEREcláusula. Luego reemplacé la prueba de expresiones regulares en la WHEREcláusula con el algoritmo Aho-Corasick implementado como un CLR. Esto redujo el tiempo total a 3 minutos y 6 segundos.

Esto requirió los siguientes cambios. Cargue el ensamblaje y las funciones para el algoritmo Aho-Corasick. Cambiar la WHEREcláusula a

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

Y agregue lo siguiente antes del primero UPDATE

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