web-dev-qa-db-ja.com

サブクエリを含む大きなテーブルでの更新が遅い

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に配置します。

このクエリをかなり速く実行する方法についていくつかの提案をお願いします。

16
Matthew Lehrer

これは計算コストが高い問題であると他のコメント者に同意しますが、使用しているSQLを微調整することで改善の余地はたくさんあると思います。説明のために、15MMの名前と3Kのフレーズで偽のデータセットを作成し、古いアプローチを実行し、新しいアプローチを実行しました。

偽のデータセットを生成して新しいアプローチを試すための完全なスクリプト

TL; DR

私のマシンとこの偽のデータセットでは、元のアプローチの実行には約4時間かかります。提案された新しいアプローチには約10分かかり、かなりの改善が見られます。ここに提案されたアプローチの短い要約があります:

  • 名前ごとに、各文字オフセットで始まる部分文字列を生成します(最適化として、最長の不適切なフレーズの長さに制限されます)。
  • これらの部分文字列にクラスター化インデックスを作成する
  • 不正なフレーズごとに、これらの部分文字列をシークして、一致を特定します
  • 元の文字列ごとに、その文字列の1つ以上の部分文字列に一致する明確な不良フレーズの数を計算します


元のアプローチ:アルゴリズム分析

元のUPDATEステートメントの計画から、作業量は名前の数(15MM)とフレーズの数(3K)の両方に線形に比例することがわかります。したがって、名前とフレーズの両方の数を10倍にすると、全体の実行時間が約100倍遅くなります。

クエリは実際にはnameの長さにも比例します。これはクエリプランでは少し隠されていますが、テーブルスプールにシークするための「実行数」に含まれます。実際のプランでは、これがnameごとに1回だけ発生するのではなく、name内の文字オフセットごとに1回発生することがわかります。したがって、このアプローチは実行時の複雑さではO(_# names_ * _# phrases_ * _name length_)です。

enter image description here


新しいアプローチ:コード

このコードは完全な 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
_


新しいアプローチ:クエリプラン

まず、各文字オフセットで始まる部分文字列を生成します

enter image description here

次に、これらの部分文字列にクラスター化インデックスを作成します

enter image description here

次に、不適切なフレーズごとに、これらの部分文字列を検索して、一致を特定します。次に、その文字列の1つまたは複数の部分文字列に一致する明確な不良フレーズの数を計算します。これは本当に重要なステップです。部分文字列にインデックスを付けた方法により、不適切な語句と名前の完全なクロス積をチェックする必要がなくなりました。実際の計算を行うこのステップは、実際の実行時間の約10%しか占めていません(残りは部分文字列の前処理です)。

enter image description here

最後に、_LEFT OUTER JOIN_を使用して実際の更新ステートメントを実行し、不適切なフレーズが見つからなかったすべての名前に0のカウントを割り当てます。

enter image description here


新しいアプローチ:アルゴリズム分析

新しいアプローチは、前処理とマッチングの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行すべてを試すのは賢明ではありません。
  • 私はSQL 2005マシンにアクセスできないため、SQL 2005ではなくSQL 2014を使用しています。 SQL 2005では使用できない構文を使用しないように注意してきましたが、SQL 2012+の tempdb lazy write 機能と parallel SELECT INTO SQL 2014の機能。
  • 名前とフレーズの長さは、新しいアプローチにとって非常に重要です。悪いフレーズは実際のユースケースと一致する可能性が高いため、通常はかなり短いと想定しています。名前は悪いフレーズよりもかなり長いですが、数千の文字ではないと想定されています。これは公平な仮定だと思います。名前の文字列が長いと、元のアプローチも遅くなります。
  • 改善の一部(ただし、すべてに近いわけではありません)は、新しいアプローチが(シングルスレッドで実行される)古いアプローチよりも効果的に並列処理を活用できるためです。私はクアッドコアのラップトップを使用しているので、これらのコアを使用できるアプローチがあるのは素晴らしいことです。


関連ブログの投稿

Aaron Bertrand このタイプのソリューションについては、彼のブログ投稿で詳細に説明しています 先頭の%wildcardのインデックスシークを取得する1つの方法

21
Geoff Patterson

Aaron Bertrand がコメントで明らかにした明白な問題を少しの間棚上げしましょう:

したがって、テーブルを3K回スキャンし、すべての15MM行をすべて3K回更新する可能性があります。

サブクエリが両側でワイルドカードを使用するという事実 劇的に検索可能性に影響を与える 。そのブログ投稿から引用するには:

つまり、SQL ServerはProductテーブルからすべての行を読み取り、名前のどこかに「ナット」があるかどうかを確認してから、結果を返す必要があります。

SourceTableの各「悪い単語」と「製品」の単語「nut」を交換し、それをアーロンのコメントと組み合わせると、それがである理由がわかります。 hard(読み取り不可能)現在のアルゴリズムを使用してすばやく実行する。

いくつかのオプションが表示されます:

  1. ビジネスを説得して、非常に強力な能力を持つモンスターサーバーを購入して、ブルートフォースによってクエリを克服するようにします。 (それは起こりそうもないので、他のオプションの方が優れています)
  2. 既存のアルゴリズムを使用して、一度痛みを受け入れ、それを広げます。これには、挿入時に不良ワードを計算する必要があり、挿入が遅くなり、新しい不良ワードが入力/検出されたときにのみテーブル全体が更新されます。
  3. ジェフの答えを受け入れる 。これは優れたアルゴリズムであり、私が思いついたものよりもはるかに優れています。
  4. オプション2を実行しますが、アルゴリズムをGeoffのものに置き換えます。

要件に応じて、オプション3または4をお勧めします。

6
Erik

まずそれはただの奇妙なアップデートです

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を使用します

0
paparazzo