Posso refatorar esta consulta para que ela seja executada em paralelo?

12

Tenho uma consulta que leva cerca de 3 horas para ser executada em nosso servidor - e não tira proveito do processamento paralelo. (cerca de 1,15 milhão de registros em dbo.Deidentified, 300 registros em dbo.NamesMultiWord). O servidor tem acesso 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;

e ReplaceMultiwordé um procedimento definido como:

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

É o chamado para ReplaceMultiwordimpedir a formação de um plano paralelo? Existe uma maneira de reescrever isso para permitir o paralelismo?

ReplaceMultiword é executado em ordem decrescente, porque algumas das substituições são versões curtas de outras e eu quero que a correspondência mais longa seja bem-sucedida.

Por exemplo, pode haver 'George Washington University' e outro da 'Washington University'. Se a partida da "Universidade de Washington" fosse a primeira, então "George" seria deixado para trás.

plano de consulta

Tecnicamente, eu posso usar o CLR, mas não estou familiarizado com isso.

rsjaffe
fonte
3
A atribuição de variável apenas definiu o comportamento para uma única linha. Não SELECT @var = REPLACE ... ORDER BYé garantido que a construção funcione conforme o esperado. Exemplo de item de conexão (consulte a resposta da Microsoft). Portanto, a mudança para o SQLCLR tem a vantagem adicional de garantir resultados corretos, o que é sempre bom.
Paul White 9

Respostas:

11

A UDF está impedindo o paralelismo. Também está causando esse carretel.

Você pode usar o CLR e um regex compilado para fazer sua pesquisa e substituição. Ele não bloqueia o paralelismo enquanto os atributos necessários estiverem presentes e provavelmente será significativamente mais rápido do que executar 300 REPLACEoperações TSQL por chamada de função.

O código de exemplo está abaixo.

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; 

Isso depende da existência de um CLR UDF como abaixo (isso DataAccessKind.Nonedeve significar que o spool desaparece e também existe para a proteção do Halloween e não é necessário, pois isso não acessa a tabela 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
fonte
Eu apenas comparei isso. Usando a mesma tabela e conteúdo para cada um, o CLR levou 3: 03.51 para processar as 1.174.731 linhas e o UDF levou 3: 16.21. Isso economizou tempo. Na minha leitura casual, parece que o SQL Server detesta paralelizar consultas UPDATE.
rsjaffe
@rsjaffe decepcionante. Eu teria esperado um resultado muito melhor do que isso. Qual é o tamanho dos dados envolvidos? (Soma do comprimento dos dados de todas as colunas afetadas)
Martin Smith
608 milhões de caracteres, 1.216 GB, o formato é NVARCHAR. Eu estava pensando em adicionar uma wherecláusula usando um teste para corresponder ao regex, pois a maioria das gravações é desnecessária - a densidade de 'hits' deve ser baixa, mas minhas habilidades em C # (eu sou um cara de C ++) não me leve lá. Eu estava pensando na linha de um procedimento public static SqlBoolean CanReplaceMultiWord(SqlString inputString, SqlXml replacementSpec)que retornaria, return Regex.IsMatch(inputString.ToString()); mas eu recebo erros nessa instrução de retorno, como `System.Text.RegularExpressions.Regex é um tipo, mas é usado como uma variável.
rsjaffe
4

Conclusão : adicionar critérios à WHEREcláusula e dividir a consulta em quatro consultas separadas, uma para cada campo, permitiu ao SQL Server fornecer um plano paralelo e fez com que a consulta fosse executada 4X o mais rápido que havia, sem o teste extra da WHEREcláusula. Dividir as consultas em quatro sem o teste não fez isso. Nem foi adicionado o teste sem dividir as consultas. A otimização do teste reduziu o tempo total de execução para 3 minutos (a partir das 3 horas originais).

Minha UDF original levou 3 horas e 16 minutos para processar 1.174.731 linhas, com 1.216 GB de dados nvarchar testados. Usando o CLR fornecido por Martin Smith em sua resposta, o plano de execução ainda não era paralelo e a tarefa levou 3 horas e 5 minutos. CLR, plano de execução não paralelo

Tendo lido esses WHEREcritérios, poderia ajudar a empurrar um UPDATEpara paralelo, fiz o seguinte. Adicionei uma função ao módulo CLR para ver se o campo correspondia ao 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, internal class ReplaceSpecificationadicionei o código para executar o teste no regex

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

Se todos os campos forem testados em uma única instrução, o SQL Server não paralelizará o trabalho

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 para executar mais de 4 horas e meia e ainda em execução. Plano de execução: Teste adicionado, declaração única

No entanto, se os campos forem separados em instruções separadas, um plano de trabalho paralelo será usado, e o uso da minha CPU passará de 12% nos planos seriais para 100% nos planos 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;

Tempo para executar 46 minutos. As estatísticas de linha mostraram que cerca de 0,5% dos registros tinham pelo menos uma correspondência de regex. Plano de execução: insira a descrição da imagem aqui

Agora, o principal problema no tempo era a WHEREcláusula. Substituí o teste regex na WHEREcláusula pelo algoritmo Aho-Corasick implementado como um CLR. Isso reduziu o tempo total para 3 minutos e 6 segundos.

Isso exigiu as seguintes alterações. Carregue a montagem e as funções do algoritmo Aho-Corasick. Mude a WHEREcláusula para

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

E adicione o seguinte antes do primeiro UPDATE

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