これは計算コストの高い問題であると他のコメント者にも同意しますが、使用しているSQLを微調整することで改善の余地があると思います。説明のために、15MMの名前と3Kフレーズで偽のデータセットを作成し、古いアプローチを実行し、新しいアプローチを実行しました。
偽のデータセットを生成し、新しいアプローチを試すための完全なスクリプト
TL; DR
私のマシンとこの偽のデータセットでは、元のアプローチの実行には約4時間かかります。提案された新しいアプローチには約10分かかり、かなり改善されます。提案されたアプローチの短い要約はここにあります:
- 各名前について、各文字オフセットで始まる部分文字列を生成します(最適化として、最長の不良フレーズの長さで制限します)
- これらの部分文字列にクラスター化インデックスを作成します
- 悪いフレーズごとに、これらの部分文字列を検索して、一致するものを特定します
- 元の文字列ごとに、その文字列の1つ以上の部分文字列に一致する明確な不良フレーズの数を計算します
元のアプローチ:アルゴリズム分析
元のUPDATE
ステートメントの計画から、作業量は名前の数(15MM)とフレーズの数(3K)の両方に直線的に比例することがわかります。したがって、名前とフレーズの両方の数を10倍すると、全体の実行時間は最大で100倍遅くなります。
クエリは実際には長さにも比例しname
ます。これはクエリプランには少し隠されていますが、テーブルスプールをシークするための「実行数」に含まれています。実際のプランでは、これはに1回だけではなくname
、実際に内の文字オフセットごとに1 回発生することがわかりますname
。したがって、このアプローチは、実行時の複雑さにおいてO(# names
* # phrases
* name length
)です。
新しいアプローチ:コード
このコードは完全なペーストビンでも使用できますが、便宜上ここにコピーしました。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%のみを占めます(残りはサブストリングの前処理です)。
最後に、a 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倍の時間がかかります。ただし、新しい実装では、サブリニアにスケールアップし、実際には、30Kの不良フレーズにスケールアップした場合、3Kの不良フレーズで測定した時間の2倍未満しかかかりません。
前提条件/注意事項
- 全体の作業を適度なサイズのバッチに分割しています。これはおそらくどちらのアプローチにとっても良いアイデアですが
SORT
、サブストリングのonが各バッチに依存せず、メモリに簡単に収まるように、新しいアプローチでは特に重要です。必要に応じてバッチサイズを操作できますが、1つのバッチで15MM行すべてを試すのは賢明ではありません。
- SQL 2005マシンにアクセスできないため、SQL 2005ではなくSQL 2014を使用しています。SQL 2005で使用できない構文を使用しないように注意しましたが、SQL 2012+のtempdb遅延書き込み機能とSQL 2014の並列SELECT INTO機能の恩恵を受ける可能性があります。
- 新しいアプローチでは、名前とフレーズの両方の長さが非常に重要です。悪いフレーズは実世界のユースケースと一致する可能性が高いため、通常は短いフレーズであると想定しています。名前は悪いフレーズよりもかなり長いですが、数千文字ではないと想定されています。これは公正な仮定であり、名前の文字列が長いと元のアプローチも遅くなると思います。
- 改善の一部(すべてに近いものはありません)は、新しいアプローチが(シングルスレッドで実行される)古いアプローチよりも効率的に並列処理を活用できるという事実によるものです。私はクアッドコアラップトップを使用しているため、これらのコアを使用できるアプローチをとることは素晴らしいことです。
関連ブログ投稿
Aaron Bertrandは、このタイプのソリューションについて、彼のブログ投稿でより詳細に調査しています。主要な%wildcardのインデックスシークを取得する1つの方法です。