サーバーでの実行に約3時間かかるクエリがあります-並列処理を利用していません。 (dbo.Deidentified
には約115万レコード、dbo.NamesMultiWord
には300レコード)。サーバーは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を使用できますが、その方法に慣れていません。
ボトムライン:WHERE
句に基準を追加し、クエリを4つの個別のクエリに分割します。各フィールドに1つずつ、SQLサーバーが提供できます並列プランを作成し、WHERE
句で追加のテストを行わずに、クエリを4倍の速さで実行しました。テストなしでクエリを4つに分割しても、それはできませんでした。クエリを分割せずにテストを追加することもしませんでした。テストを最適化すると、合計実行時間が3分に減少しました(元の3時間から)。
私の元のUDFは、1,174,731行を処理するのに3時間16分かかり、1.216 GBのnvarcharデータがテストされました。マーティン・スミスから提供されたCLRを回答に使用したところ、実行計画はまだ並列ではなく、タスクは3時間5分かかりました。
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時間半以上実行され、まだ実行されている時間。実行計画: -
ただし、フィールドが個別のステートメントに分割されている場合、並列作業計画が使用され、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'
);
UDFが並列処理を妨げています。それはまたそのスプールを引き起こしています。
CLRとコンパイル済みの正規表現を使用して、検索と置換を行うことができます。並列処理はブロックされません 必要な属性が存在する限り であり、関数呼び出しごとに300 TSQL REPLACE
操作を実行するよりもはるかに高速です。
コード例は以下のとおりです。
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);
}
});
}
}
}