이 쿼리를 리팩터링하여 병렬로 실행할 수 있습니까?


12

서버에서 실행하는 데 약 3 시간이 걸리는 쿼리가 있는데 병렬 처리를 이용하지 않습니다. (에서 약 1,150 만 개의 레코드, dbo.Deidentified에서 300 개의 레코드 dbo.NamesMultiWord). 서버는 8 개의 코어에 액세스 할 수 있습니다.

  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;

다음과 ReplaceMultiword같이 정의 된 절차입니다.

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

ReplaceMultiword병행 계획을 세우지 않겠다는 요청이 있습니까? 병렬 처리를 위해 이것을 다시 쓰는 방법이 있습니까?

ReplaceMultiword 일부 교체는 다른 교체의 짧은 버전이기 때문에 내림차순으로 실행되며 가장 긴 일치가 성공하기를 원합니다.

예를 들어 'George Washington University'와 'Washington University'의 다른 것이있을 수 있습니다. '워싱턴 대학교'경기가 처음이라면 'George'가 남게됩니다.

쿼리 계획

기술적으로 CLR을 사용할 수 있지만 그렇게하는 방법에 익숙하지 않습니다.


3
변수 할당은 단일 행에 대해서만 동작을 정의했습니다. SELECT @var = REPLACE ... ORDER BY예상대로 건설이 작동하지 않을 수 있습니다. 연결 항목 예 (Microsoft의 응답 참조). 따라서 SQLCLR로 전환하면 올바른 결과를 보장 할 수 있다는 이점이 있습니다.
Paul White 9

답변:


11

UDF가 병렬 처리를 방지하고 있습니다. 또한 스풀을 발생시킵니다.

CLR과 컴파일 된 정규식을 사용하여 검색 및 교체를 수행 할 수 있습니다. 필요한 속성이 존재하는 한 병렬 처리 차단하지 않으며 REPLACE함수 호출 당 300 TSQL 작업을 수행하는 것보다 훨씬 빠를 것 입니다.

예제 코드는 다음과 같습니다.

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; 

이는 아래와 같이 CLR UDF의 존재 여부에 따라 다릅니다 ( DataAccessKind.None스풀이 사라지고 할로윈 보호를 위해 존재하며 대상 테이블에 액세스하지 않으므로 필요하지 않음을 의미 함).

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

방금 벤치마킹했습니다. 각각에 대해 동일한 테이블과 내용을 사용하여 CLR은 3 : 03.51을 사용하여 1,174,731 행을 처리하고 UDF는 3 : 16.21을 수행했습니다. 시간을 절약했습니다. 필자의 일상적인 독서에서 SQL Server가 UPDATE 쿼리를 병렬화하는 것을 싫어하는 것처럼 보입니다.
rsjaffe

@rsjaffe 실망. 나는 그것보다 훨씬 더 나은 결과를 원했을 것입니다. 관련된 데이터의 크기는 얼마입니까? (영향을받는 모든 열의 데이터 길이의 합)
Martin Smith

6 억 1,600 만 자, 1.216 GB, 형식은 NVARCHAR입니다. 필자는 where대부분의 쓰기가 불필요하기 때문에 정규 표현식과 일치하는 테스트를 사용하여 절 을 추가하려고 생각 했습니다. 거기로 데려다 줘 나는 public static SqlBoolean CanReplaceMultiWord(SqlString inputString, SqlXml replacementSpec)리턴 할 프로 시저 라인을 따라 생각 return Regex.IsMatch(inputString.ToString()); 했지만`System.Text.RegularExpressions.Regex와 같은 리턴 문에 오류가 발생하지만 변수처럼 사용됩니다.
rsjaffe

4

결론 : WHERE절에 기준을 추가 하고 쿼리를 4 개의 개별 쿼리로 나누면 각 필드마다 하나씩 SQL Server가 병렬 계획을 제공하고 WHERE절 에서 추가 테스트없이 4 배 빠르게 쿼리를 실행할 수있었습니다 . 테스트없이 쿼리를 4로 나누면 그렇게되지 않았습니다. 쿼리를 나누지 않고 테스트를 추가하지 않았습니다. 테스트를 최적화하면 총 실행 시간이 원래 3 시간에서 3 분으로 줄었습니다.

내 원래 UDF는 1,174,731 개의 행을 처리하는 데 3 시간 16 분이 걸렸으며 1.216GB의 nvarchar 데이터가 테스트되었습니다. Martin Smith가 제공 한 CLR을 사용하여 실행 계획은 여전히 ​​평행하지 않았고 작업에는 3 시간 5 분이 걸렸습니다. CLR, 실행 계획이 병렬이 아님

WHERE기준 을 읽으면 UPDATE병렬화에 도움이 될 수 있습니다. 필드가 정규식과 일치하는지 확인하기 위해 CLR 모듈에 함수를 추가했습니다.

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

와,에 internal class ReplaceSpecification, 나는 정규 표현식에 대해 테스트를 실행하는 코드를 추가

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

모든 필드가 단일 명령문으로 테스트되면 SQL Server가 작업을 병렬화하지 않습니다.

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

4 1/2 시간 이상 실행되고 여전히 실행되는 시간. 실행 계획 : 테스트 추가, 단일 진술

그러나 필드가 별도의 문으로 분리되면 병렬 작업 계획이 사용되고 내 CPU 사용량은 직렬 계획의 경우 12 %에서 병렬 계획 (8 개의 코어)의 경우 100 %로 변경됩니다.

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;

46 분 동안 실행합니다. 행 통계에 따르면 레코드의 약 0.5 %가 최소한 하나의 정규식 일치를 나타 냈습니다. 실행 계획 : 여기에 이미지 설명을 입력하십시오

이제 시간의 주요 드래그는 WHERE조항이었습니다. 그런 다음 WHERE절의 정규식 테스트를 CLR로 구현 된 Aho-Corasick 알고리즘으로 대체했습니다 . 이로써 총 시간이 3 분 6 초로 단축되었습니다.

이를 위해서는 다음과 같은 변경이 필요했습니다. Aho-Corasick 알고리즘의 어셈블리 및 함수를로드하십시오. WHERE절을 다음으로 변경하십시오.

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

그리고 첫 번째 전에 다음을 추가하십시오 UPDATE

DECLARE @ac NVARCHAR(32);
SET @ac = dbo.CreateAhoCorasick(
  (SELECT NAMES FROM dbo.NamesMultiWord FOR XML RAW, root('root')),
  'en-us:i'
);
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.