Webアプリケーションをサポートするストアドプロシージャを持つレガシーシステムを継承しました。 1日に数千回実行されます。このアプリケーションでは、ユーザーは顧客の詳細の一部を入力でき、基になるストアドプロシージャはLIKEコマンドを実行して、可能な顧客の候補リストをWebアプリに返します。
顧客ID(整数値)の部分的な入力も可能であるため、値 '123'がアプリに入力された場合、ストアドプロシージャは '123'を含むIDを持つすべての顧客、つまり '612345'または '222123を返す必要があります'.....はい、クレイジーだとわかっています。「1」と入力すると膨大なデータセットが返されますが、アプリを変更することはできません。
カスタマーIDの検索は、ストアード・プロシージャーの最も遅い部分です。ストアドプロシージャ(DEV内)を "最適化"したため、使用するIOおよび使用するCPUが少なくなりましたが、シリアルがなくなり、実行に時間がかかりすぎます.....ヘルプ!
問題を再現する方法は次のとおりです。
--CREATE TEST TABLE
CREATE TABLE dbo.Test(ID INT IDENTITY(1,1) PRIMARY KEY
, Val Float
, CodeVal CHAR(100)
)
GO
--GENERATE A FEW MILLION TEST RECORD
INSERT INTO dbo.Test
SELECT
Rand()
,CONVERT(varchar(255), NEWID())
FROM
sys.objects --Contains 639 Rows
GO 10000
--CREATE INDEX ON ID FIELD
CREATE UNIQUE NONCLUSTERED INDEX idx ON dbo.Test(ID)
既存のクエリは、パラレルINDEX SCANを実行します。
DECLARE @ID VARCHAR(20) = '123456'
--DROP TABLE #TMP1
CREATE TABLE #TMP1(ID INT)
INSERT INTO #TMP1
SELECT ID
FROM dbo.Test
WHERE CONVERT(VARCHAR(20),ID) LIKE '%'+@ID+'%'
次に、ユーザーが「123456」という顧客IDの一部を入力すると、結果セットに「123456」より小さい値が含まれることはないことに気付きました。そこで、コードに追加の行を追加し(下記を参照)、IOとCPU時間を削減したINDEX SEEKを取得しました。しかし、恐ろしいことに、シリアルがなくなったので、今では永遠にかかります。これがライブになると、ユーザーの待機時間は4秒から20秒にジャンプします。
INSERT INTO #TMP1
SELECT ID
FROM dbo.Test
WHERE CONVERT(VARCHAR(20),ID) LIKE '%'+@ID+'%'
AND ID >= @ID --New line of code
並列処理ができることを願って盲目的にコーディングを行うようになりました。
なぜこれが起こっているのか誰でも説明できますか(「オプティマイザはそれがより良いと決定した」という株式の回答は別として)?
そして、誰でもクエリを並列化する方法を理解できますか?
トレースフラグ8649はオプションではありません。
そして、私はすでに Paul White によるこの非常に役立つ記事を読みました。
UPDATE:
提供されている例では、テストテーブルのシステム仕様と行数の組み合わせに応じてさまざまな結果が生成されるようです。
問題を再現できない場合は、お詫び申し上げます。しかし、これは私の問題に対する答えの一部です。
UPDATE :(実行計画)
Aaron:ご使用のシステムで機能しない場合、例としてイライラします(InfoSecの理由で、会社の実際の実行計画を共有できません)。
自宅のシステムで問題を再現しました。
実行計画は次のとおりです(ポストプランに完全にアップロードします)
現在のdbo.Testの行数は2124160、非クラスター化インデックスには2627ページ、0.04%の断片化があります。以下は統計情報です。
SQL Server Execution Times:
CPU time = 0 ms, elapsed time = 1 ms.
SQL Server parse and compile time:
CPU time = 0 ms, elapsed time = 1 ms.
Table 'Test'. Scan count 3, logical reads 2652, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
(1 row(s) affected)
(1 row(s) affected)
SQL Server Execution Times:
CPU time = 688 ms, elapsed time = 368 ms.
SQL Server parse and compile time:
CPU time = 0 ms, elapsed time = 2 ms.
Table 'Test'. Scan count 1, logical reads 1106, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
(1 row(s) affected)
(1 row(s) affected)
SQL Server Execution Times:
CPU time = 281 ms, elapsed time = 302 ms.
SQL Server parse and compile time:
CPU time = 0 ms, elapsed time = 0 ms.
このストアドプロシージャは、顧客を担当するコールセンターのスタッフが1日に約5000回使用します。 16秒の待機時間の増加は、結果を待つスタッフの1日あたり22時間強に相当します。これは8時間のシフト時間で、フルタイムの3人のスタッフが年間約36,000ポンドの費用を負担することに相当します。 SQL Sentryライセンス(大ファン)にアップグレードするにはお金がかかりますが、現在Spotlight(まだ良い製品)がありますが、もっと深いものが欲しいです。
更新:(マジックナンバー)
12,000行では、両方のクエリがシリアルでした。
2,124,160行の場合、1つのクエリは並列になります。
66,000,000行では、両方が並列になります。
オプティマイザーが使用するルール/メトリックが確実にあり、インデックススキャンまたはインデックスシークがある場合は異なります。 これは、クエリをスキャンからシークに変換すると、特定のサイズのテーブル/インデックスに悪影響を及ぼす可能性があることを意味しますか?
UPDATE:(ソリューション)
Joeに感謝します。あなたはMax Degree of Parallelismにスポットを当てていました。
私が使用した:
OPTION (OPTIMIZE FOR (@ID = 1))
UNKNOWNは私の環境ではうまくいかなかった。
興味のある方は、 Kendra Little による便利な記事をご覧ください。
並列処理のコストしきい値 パラメータの問題が発生していると思います。
並列処理のコストしきい値オプションを使用して、Microsoft SQL Serverがクエリの並列プランを作成および実行するしきい値を指定します。 SQL Serverは、同じクエリのシリアルプランを実行するための推定コストが、並列処理のコストしきい値に設定された値よりも高い場合にのみ、クエリのパラレルプランを作成して実行します。
より長いクエリは通常、並列プランの恩恵を受けます。パフォーマンス上の利点は、並列プランの初期化、同期、および終了に必要な追加の時間を打ち消します。並列処理オプションのコストしきい値は、短いクエリと長いクエリが混在して実行されるときに積極的に使用されます。短いクエリはシリアルプランを実行しますが、長いクエリはパラレルプランを使用します。並列処理のコストしきい値の値は、どのクエリが短いと見なされるかを決定するため、シリアルプランを使用して実行する必要があります。
テストマシンでは、cost threshold for parallelism
のデフォルト値は5です。以下は、古いクエリと新しいクエリのMAXDOP 1
コストの表です。
╔═════════╦═══════════════════╦═══════════════════╗
║ Rows ║ Cost of Old Query ║ Cost of New Query ║
╠═════════╬═══════════════════╬═══════════════════╣
║ 12000 ║ 0.037717 ║ 0.0199238 ║
║ 1610200 ║ 6.28954 ║ 1.86659 ║
║ 4610200 ║ 17.9959 ║ 6.46108 ║
╚═════════╩═══════════════════╩═══════════════════╝
予想通り、1610200行の場合、古いクエリは並列になりますが、新しいクエリは連続します。これは、DOP 1のコストが5を超えていたため、並列プランに適格であったためです。 SQL Serverは、並列プランのコストを低く見積もったので、並列になりました。 4610200行では、両方のクエリが並列になります。ご使用のマシンでは、少し異なる結果が表示される場合があります。
ここで並列プランを取得することに反対する2つの点があります。 1つ目は、SQL Serverには、このクエリがビジネスにとって不可欠であり、できるだけ多くのリソースを投入する必要があることを知る方法がないことです。それはあなたの組織が数万ドルを失う可能性があるというあなたの見積もりについては知りません。それを別の安くて簡単なクエリと見なすだけです。 2つ目は、@ID
の値に関係なく、クエリオプティマイザーが常にクエリに同じ推定コストを割り当てることです。変数を使用しているため、デフォルトの推定値が使用されます。 RECOMPILE
ヒントで回避できますが、これが1日に数千回実行される場合は、最善の方法ではない可能性があります。
私の推奨は、OPTIMIZE FOR
クエリヒントを使用して、クラスター化インデックスシークからの推定行数を増やすことです。これにより、プランのコストが増加し、並列プランを使用する可能性が高くなります。開始 [〜#〜] bol [〜#〜] :
OPTIMIZE FOR(@variable_name {UNKNOWN | = literal_constant} [、... n])
クエリがコンパイルおよび最適化されるときにローカル変数に特定の値を使用するようにクエリオプティマイザーに指示します。この値は、クエリの最適化中にのみ使用され、クエリの実行中には使用されません。
@variable_name
クエリで使用されるローカル変数の名前です。OPTIMIZEFORクエリヒントで使用するために値を割り当てることができます。
literal_constant
OPTIMIZE FORクエリヒントで使用するために@variable_nameに割り当てられるリテラル定数値です。 literal_constantはクエリの最適化中にのみ使用され、クエリ実行中の@variable_nameの値としては使用されません。 literal_constantは、リテラル定数として表現できる任意のSQL Serverシステムデータ型にすることができます。 literal_constantのデータ型は、クエリで@variable_nameが参照するデータ型に暗黙的に変換できる必要があります。
クエリでは、'1'
の値を最適化できます。それは計画のコストをかなり増やすはずです。実際、私のテストでは、これによりクエリはフィルターなしの古いクエリと同じ推定コストになります。 SQL ServerはID > '1'
の述語を含む行をフィルターで除外できないため、これは理にかなっています。このヒントを使用すると、コードを変更する前と同じパラレル/シリアル動作が発生するはずです。
DECLARE @ID VARCHAR(20) = '123456';
CREATE TABLE #TMP1 (ID INT);
INSERT INTO #TMP1 -- cost is 96.8547
SELECT ID
FROM dbo.Test
WHERE CONVERT(VARCHAR(20),ID) LIKE '%'+@ID+'%'
AND ID >= @ID
OPTION (OPTIMIZE FOR (@ID = '1'));
クエリがより複雑になると、このヒントが悪影響を与える可能性があることに注意してください。たとえば、推定行数を人為的に増やすと、必要以上のクエリメモリ許可が発生する可能性があります。運用データが十分に大きく、ヒントが不要な場合は、使用しないことをお勧めします。本番環境のクエリが質問に記載されているものと異なる場合は、使用前に慎重にテストしてください。