行を返さないクエリにORDER BYを含めると、パフォーマンスに大きく影響します


15

単純な3つのテーブルの結合を考えると、行が返されない場合でもORDER BYを含めると、クエリのパフォーマンスが大幅に変わります。実際の問題シナリオでは、ゼロ行を返すのに30秒かかりますが、ORDER BYが含まれていない場合は即座に発生します。どうして?

SELECT * 
FROM tinytable t                          /* one narrow row */
JOIN smalltable s on t.id=s.tinyId        /* one narrow row */
JOIN bigtable b on b.smallGuidId=s.GuidId /* a million narrow rows */
WHERE t.foreignId=3                       /* doesn't match */
ORDER BY b.CreatedUtc          /* try with and without this ORDER BY */

bigtable.smallGuidIdにインデックスを作成できることは理解していますが、この場合は実際にインデックスが悪化するものと考えています。

テスト用のテーブルを作成/設定するスクリプトを次に示します。奇妙なことに、smalltableにはnvarchar(max)フィールドがあることが問題のようです。また、GUIDを使用してbigtableに参加していることも重要なようです(これにより、ハッシュマッチングを使用したいと思うでしょう)。

CREATE TABLE tinytable
  (
     id        INT PRIMARY KEY IDENTITY(1, 1),
     foreignId INT NOT NULL
  )

CREATE TABLE smalltable
  (
     id     INT PRIMARY KEY IDENTITY(1, 1),
     GuidId UNIQUEIDENTIFIER NOT NULL DEFAULT NEWID(),
     tinyId INT NOT NULL,
     Magic  NVARCHAR(max) NOT NULL DEFAULT ''
  )

CREATE TABLE bigtable
  (
     id          INT PRIMARY KEY IDENTITY(1, 1),
     CreatedUtc  DATETIME NOT NULL DEFAULT GETUTCDATE(),
     smallGuidId UNIQUEIDENTIFIER NOT NULL
  )

INSERT tinytable
       (foreignId)
VALUES(7)

INSERT smalltable
       (tinyId)
VALUES(1)

-- make a million rows 
DECLARE @i INT;

SET @i=20;

INSERT bigtable
       (smallGuidId)
SELECT GuidId
FROM   smalltable;

WHILE @i > 0
  BEGIN
      INSERT bigtable
             (smallGuidId)
      SELECT smallGuidId
      FROM   bigtable;

      SET @i=@i - 1;
  END 

SQL 2005、2008、2008R2で同じ結果をテストしました。

回答:


32

私はマーティン・スミスの答えに同意しますが、問題は正確に統計の単なるものではありません。foreignId列の統計(自動統計が有効になっていると仮定)は、値3の行が存在しないことを正確に示します(値が1で、値が7)。

DBCC SHOW_STATISTICS (tinytable, foreignId) WITH HISTOGRAM

統計出力

SQL Serverは、統計がキャプチャされてから状況が変化した可能性があることを認識しているため、プランの実行時に値3の行が存在する場合があります。さらに、プランのコンパイルと実行の間に時間がかかる場合があります(結局、プランは再利用のためにキャッシュされます)。Martinが言うように、SQL Serverには、最適化のためにキャッシュされたプランの再コンパイルを正当化するために十分な変更が行われたことを検出するロジックが含まれています。

ただし、これは最終的に重要ではありません。エッジケースの例外が1つある場合、オプティマイザーはテーブル操作によって生成される行の数をゼロとは決して推定しません。出力を常にゼロ行にする必要があると静的に判断できる場合、操作は冗長であり、完全に削除されます。

オプティマイザーのモデルは、代わりに最小 1行を推定します。このヒューリスティックを使用すると、より低い推定が可能である場合よりも、平均してより良い計画を作成する傾向があります。コストベースの判断を下す根拠がないため、ある段階でゼロ行の推定値を生成する計画は、処理ストリームのその時点からは役に立ちません(ゼロ行は何があってもゼロ行です)。推定値が間違っていることが判明した場合、ゼロ行の推定値より上の計画形状は、妥当である可能性がほとんどありません。

2番目の要因は、閉じ込めの仮定と呼ばれる別のモデリングの仮定です。これは基本的に、クエリが値の範囲を別の値の範囲と結合する場合、範囲が重複しているためであると言います。これを記述する別の方法は、行が返されることが期待されるため、結合が指定されていると言うことです。この推論がなければ、コストは一般的に過小評価され、その結果、一般的なクエリの広範な計画が不十分になります。

基本的に、ここにあるのは、オプティマイザーのモデルに適合しないクエリです。複数列インデックスまたはフィルター処理されたインデックスを使用して推定値を「改善」するためにできることは何もありません。ここでは、1行未満の推定値を取得する方法はありません。実際のデータベースには、このような状況が発生しないようにするための外部キーが含まれている場合がありますが、ここでは適用できないと仮定すると、ヒントを使用してモデル外の状態を修正します。このクエリでは、さまざまなヒントアプローチを使用できます。 OPTION (FORCE ORDER)記述されたクエリでうまく機能するものです。


21

ここでの基本的な問題は、統計の1つです。

両方のクエリについて、推定行数は、実際SELECTに結果として生じるbigtable0ではなく、最終が1,048,580行(に存在すると推定される行の同じ数)を返すと信じていることを示しています。

両方のJOIN条件が一致し、すべての行が保持されます。単一の行が述部とtinytable一致しないため、最終的に削除されt.foreignId=3ます。

走ったら

SELECT * 
FROM tinytable t  
WHERE t.foreignId=3  AND id=1 

予想される行数では1なく0、このエラーが計画全体に伝播します。tinytable現在、1行が含まれています。500行の変更が発生するまで、このテーブルの統計は再コンパイルされないため、一致する行を追加でき、再コンパイルはトリガーされません。

ORDER BY句を追加してvarchar(max)列があるときに結合順序が変更される理由は、列によって行サイズが平均で4,000バイト増加するsmalltableと推定されるためvarchar(max)です。それに1048580行を掛けると、ソート操作に推定4GBが必要になるためSORTJOIN

以下のヒントを使用ORDER BYして、クエリに強制的に非ORDER BY結合戦略を採用させることができます。

SELECT *
FROM   tinytable t /* one narrow row */
       INNER MERGE JOIN smalltable s /* one narrow row */
                        INNER LOOP JOIN bigtable b
                          ON b.smallGuidId = s.GuidId /* a million narrow rows */
         ON t.id = s.tinyId
WHERE  t.foreignId = 3 /* doesn't match */
ORDER  BY b.CreatedUtc
OPTION (MAXDOP 1) 

このプランは、ほぼ12,000間違いのある推定行数と推定データサイズの推定サブツリーコストを持つソート演算子を示しています。

予定

ところでUNIQUEIDENTIFIER、私のテストでは、列を整数の列に置き換えることで、物事が変更されることはありませんでした。


2

[実行計画の表示]ボタンをオンにすると、何が起こっているのかを確認できます。「遅い」クエリの計画は次のとおりです。 ここに画像の説明を入力してください

そして、これが「高速」クエリです。 ここに画像の説明を入力してください

それを見てください-一緒に実行すると、最初のクエリは〜33xより「高価」です(比率97:3)。SQLは、最初のクエリを最適化して日付順にBigTableを並べ替え、SmallTableとTinyTableで小さな「シーク」ループを実行して、それぞれ100万回実行します(「クラスター化インデックスシーク」アイコンにカーソルを合わせると、より多くの統計情報を取得できます)。そのため、小さなテーブル(23%と46%)でのソート(27%)と2 x 100万回の "シーク"は、高価なクエリの大部分です。これに対して、非ORDER BYクエリは合計3回のスキャンを実行します。

基本的に、特定のシナリオのSQLオプティマイザーロジックに穴があります。しかし、TysHTTPで述べられているように、インデックスを追加すると(挿入/更新が遅くなります)、スキャンが非常に速くなります。


2

何が起こっているのかというと、SQLは制限の前までに注文を実行することを決定しています。

これを試して:

SELECT *
(
SELECT * 
FROM tinytable t
    INNER JOIN smalltable s on t.id=s.tinyId
    INNER JOIN bigtable b on b.smallGuidId=s.GuidId
WHERE t.foreignId=3
) X
ORDER BY b.CreatedUtc

これにより、実際に別のインデックスを追加してもパフォーマンスが低下することなく、パフォーマンスが向上します(この場合、返される結果数が非常に少ない)。SQLオプティマイザーが結合前に順序を実行することを決定するのは奇妙ですが、実際にデータを返す場合は、結合後にソートするよりも結合せずにソートする方が時間がかかるためです。

最後に、次のスクリプトを実行して、更新された統計とインデックスが問題を解決するかどうかを確認してください。

EXEC [sp_MSforeachtable] @command1="RAISERROR('UPDATE STATISTICS(''?'') ...',10,1) WITH NOWAIT UPDATE STATISTICS ? "

EXEC [sp_MSforeachtable] @command1="RAISERROR('DBCC DBREINDEX(''?'') ...',10,1) WITH NOWAIT DBCC DBREINDEX('?')"

EXEC [sp_MSforeachtable] @command1="RAISERROR('UPDATE STATISTICS(''?'') ...',10,1) WITH NOWAIT UPDATE STATISTICS ? "

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.