このクエリをリファクタリングして、並列に実行できますか?


12

サーバーで実行するのに約3時間かかるクエリがありますが、並列処理を利用していません。(で約115万レコード、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 置換の一部は他の置換の短いバージョンであり、最長一致が成功するようにするため、降順で実行されます。

たとえば、「ジョージワシントン大学」と「ワシントン大学」の他の大学があります。「ワシントン大学」の試合が最初であれば、「ジョージ」は取り残されます。

クエリプラン

技術的にはCLRを使用できますが、その方法はよくわかりません。


3
変数の割り当てでは、単一行の動作のみが定義されています。SELECT @var = REPLACE ... ORDER BYあなたが期待するような構成は、仕事に保証するものではありません。接続項目の例(Microsoftからの応答を参照)。そのため、SQLCLRに切り替えると、常に正しい結果が得られるという利点があります。
ポールホワイト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は1,174,731行を処理するのに3:03.51を使用し、UDFは3:16.21を使用しました。時間を節約しました。私のさりげない読書では、SQL ServerがUPDATEクエリの並列化を嫌うように見えます。
rsjaffe

@rsjaffeがっかり。私はそれよりもはるかに良い結果を望んでいたでしょう。関連するデータのサイズはどれくらいですか?(影響を受けるすべての列のデータ長の合計)
マーティンスミス

608百万文字、1.216 GB、形式はNVARCHARです。whereほとんどの書き込みは不要であるため、正規表現との一致テストを使用して句を追加することを考えていました-「ヒット」の密度は低くなければなりませんが、私のC#スキル(私はC ++の男です)はしませんでしたそこに着いて public static SqlBoolean CanReplaceMultiWord(SqlString inputString, SqlXml replacementSpec)戻るプロシージャの行に沿って考えていましたが、return Regex.IsMatch(inputString.ToString()); その戻りステートメントでエラーが発生しました。
rsjaffe

4

ボトムラインWHERE句に条件を追加し、クエリを4つの個別のクエリに分割します。各フィールドに1つを指定することで、SQLサーバーは並列プランを提供し、WHERE句に余分なテストを行わずにクエリを4倍速く実行できました。テストなしでクエリを4つに分割しても、それはできませんでした。クエリを分割せずにテストを追加することもしませんでした。テストの最適化により、合計実行時間が3分に短縮されました(元の3時間から)。

元のUDFは1,174,731行を処理するのに3時間16分かかり、1.216 GBの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サーバーは作業を並列化しません

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時間30分以上実行し、まだ実行中の時間。実行計画: テストを追加、単一ステートメント

ただし、フィールドが個別のステートメントに分割されている場合、並列作業計画が使用され、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%に少なくとも1つの正規表現の一致があったことが示されました。実行計画: ここに画像の説明を入力してください

さて、時間通りの主なドラッグは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.