SourceTable
に> 15MMのレコードがあり、Bad_Phrase
に> 3Kのレコードがあるため、次のクエリをSQL Server 2005 SP4で実行するには、約10時間かかります。
UPDATE [SourceTable]
SET
Bad_Count=
(
SELECT
COUNT(*)
FROM Bad_Phrase
WHERE
[SourceTable].Name like '%'+Bad_Phrase.PHRASE+'%'
)
英語では、このクエリはName
のフィールドSourceTable
のサブストリングであるBad_Phraseにリストされている個別のフレーズの数をカウントし、その結果をフィールドBad_Count
に配置します。
このクエリをかなり速く実行する方法についていくつかの提案をお願いします。
これは計算コストが高い問題であると他のコメント者に同意しますが、使用しているSQLを微調整することで改善の余地はたくさんあると思います。説明のために、15MMの名前と3Kのフレーズで偽のデータセットを作成し、古いアプローチを実行し、新しいアプローチを実行しました。
偽のデータセットを生成して新しいアプローチを試すための完全なスクリプト
TL; DR
私のマシンとこの偽のデータセットでは、元のアプローチの実行には約4時間かかります。提案された新しいアプローチには約10分かかり、かなりの改善が見られます。ここに提案されたアプローチの短い要約があります:
元のアプローチ:アルゴリズム分析
元のUPDATE
ステートメントの計画から、作業量は名前の数(15MM)とフレーズの数(3K)の両方に線形に比例することがわかります。したがって、名前とフレーズの両方の数を10倍にすると、全体の実行時間が約100倍遅くなります。
クエリは実際にはname
の長さにも比例します。これはクエリプランでは少し隠されていますが、テーブルスプールにシークするための「実行数」に含まれます。実際のプランでは、これがname
ごとに1回だけ発生するのではなく、name
内の文字オフセットごとに1回発生することがわかります。したがって、このアプローチは実行時の複雑さではO(_# names
_ * _# phrases
_ * _name length
_)です。
新しいアプローチ:コード
このコードは完全な Pastebin でも利用できますが、便宜上ここにコピーしました。 Pastebinには、現在のバッチの境界を定義するために以下に示す_@minId
_および_@maxId
_変数を含む完全なプロシージャ定義もあります。
_-- For each name, generate the string at each offset
DECLARE @maxBadPhraseLen INT = (SELECT MAX(LEN(phrase)) FROM Bad_Phrase)
SELECT s.id, sub.sub_name
INTO #SubNames
FROM (SELECT * FROM SourceTable WHERE id BETWEEN @minId AND @maxId) s
CROSS APPLY (
-- Create a row for each substring of the name, starting at each character
-- offset within that string. For example, if the name is "abcd", this CROSS APPLY
-- will generate 4 rows, with values ("abcd"), ("bcd"), ("cd"), and ("d"). In order
-- for the name to be LIKE the bad phrase, the bad phrase must match the leading X
-- characters (where X is the length of the bad phrase) of at least one of these
-- substrings. This can be efficiently computed after indexing the substrings.
-- As an optimization, we only store @maxBadPhraseLen characters rather than
-- storing the full remainder of the name from each offset; all other characters are
-- simply extra space that isn't needed to determine whether a bad phrase matches.
SELECT TOP(LEN(s.name)) SUBSTRING(s.name, n.n, @maxBadPhraseLen) AS sub_name
FROM Numbers n
ORDER BY n.n
) sub
-- Create an index so that bad phrases can be quickly compared for a match
CREATE CLUSTERED INDEX IX_SubNames ON #SubNames (sub_name)
-- For each name, compute the number of distinct bad phrases that match
-- By "match", we mean that the a substring starting from one or more
-- character offsets of the overall name starts with the bad phrase
SELECT s.id, COUNT(DISTINCT b.phrase) AS bad_count
INTO #tempBadCounts
FROM dbo.Bad_Phrase b
JOIN #SubNames s
ON s.sub_name LIKE b.phrase + '%'
GROUP BY s.id
-- Perform the actual update into a "bad_count_new" field
-- For validation, we'll compare bad_count_new with the originally computed bad_count
UPDATE s
SET s.bad_count_new = COALESCE(b.bad_count, 0)
FROM dbo.SourceTable s
LEFT JOIN #tempBadCounts b
ON b.id = s.id
WHERE s.id BETWEEN @minId AND @maxId
_
新しいアプローチ:クエリプラン
まず、各文字オフセットで始まる部分文字列を生成します
次に、これらの部分文字列にクラスター化インデックスを作成します
次に、不適切なフレーズごとに、これらの部分文字列を検索して、一致を特定します。次に、その文字列の1つまたは複数の部分文字列に一致する明確な不良フレーズの数を計算します。これは本当に重要なステップです。部分文字列にインデックスを付けた方法により、不適切な語句と名前の完全なクロス積をチェックする必要がなくなりました。実際の計算を行うこのステップは、実際の実行時間の約10%しか占めていません(残りは部分文字列の前処理です)。
最後に、_LEFT OUTER JOIN
_を使用して実際の更新ステートメントを実行し、不適切なフレーズが見つからなかったすべての名前に0のカウントを割り当てます。
新しいアプローチ:アルゴリズム分析
新しいアプローチは、前処理とマッチングの2つのフェーズに分けることができます。次の変数を定義しましょう:
N
=名前の数B
=不正なフレーズの数L
=名前の平均の長さ(文字数)前処理フェーズはO(N*L * LOG(N*L))
であり、_N*L
_サブストリングを作成してソートします。
実際のマッチングはO(B * LOG(N*L))
であり、各不適切なフレーズの部分文字列を検索します。
このようにして、不適切なフレーズの数に比例して拡大縮小しないアルゴリズムを作成しました。これは、3Kフレーズ以上に拡大すると、主要なパフォーマンスが発揮されます。別の言い方をすれば、元の実装は、300の悪いフレーズから3Kの悪いフレーズに行く限り、およそ10倍かかります。同様に、3Kの悪いフレーズを30Kにするとすると、さらに10倍の時間がかかります。ただし、新しい実装では、直線的にスケールアップし、実際に3Kの悪いフレーズで測定した時間の2倍未満の時間で、30Kの悪いフレーズにスケールアップします。
前提条件/警告
SORT
がバッチごとに独立していてメモリに簡単に収まるように、新しいアプローチでは特に重要です。必要に応じてバッチサイズを操作できますが、1つのバッチで15MM行すべてを試すのは賢明ではありません。
関連ブログの投稿
Aaron Bertrand このタイプのソリューションについては、彼のブログ投稿で詳細に説明しています 先頭の%wildcardのインデックスシークを取得する1つの方法 。
Aaron Bertrand がコメントで明らかにした明白な問題を少しの間棚上げしましょう:
したがって、テーブルを3K回スキャンし、すべての15MM行をすべて3K回更新する可能性があります。
サブクエリが両側でワイルドカードを使用するという事実 劇的に検索可能性に影響を与える 。そのブログ投稿から引用するには:
つまり、SQL ServerはProductテーブルからすべての行を読み取り、名前のどこかに「ナット」があるかどうかを確認してから、結果を返す必要があります。
SourceTable
の各「悪い単語」と「製品」の単語「nut」を交換し、それをアーロンのコメントと組み合わせると、それがである理由がわかります。 hard(読み取り不可能)現在のアルゴリズムを使用してすばやく実行する。
いくつかのオプションが表示されます:
要件に応じて、オプション3または4をお勧めします。
まずそれはただの奇妙なアップデートです
Update [SourceTable]
Set [SourceTable].[Bad_Count] = [fix].[count]
from [SourceTable]
join ( Select count(*)
from [Bad_Phrase]
where [SourceTable].Name like '%' + [Bad_Phrase].[PHRASE] + '%')
'%' + [Bad_Phrase]。[PHRASE]があなたを殺しています
インデックスを使用できません
データ設計は速度に最適ではありません
[Bad_Phrase]。[PHRASE]を1つのフレーズ/単語に分割できますか?
同じフレーズ/単語が複数表示される場合、より多くのカウントを使用する場合は、複数回入力できます。
そのため、不適切なフレーズの行数は増加します
可能であれば、これははるかに速くなります
Update [SourceTable]
Set [SourceTable].[Bad_Count] = [fix].[count]
from [SourceTable]
join ( select [PHRASE], count(*) as count
from [Bad_Phrase]
group by [PHRASE]
) as [fix]
on [fix].[PHRASE] = [SourceTable].[name]
where [SourceTable].[Bad_Count] <> [fix].[count]
2005がそれをサポートしているかどうかはわかりませんが、フルテキストインデックスを含み、Containsを使用します