web-dev-qa-db-ja.com

統計が更新されるまでクエリ実行プランは恐ろしい

皆さんが私をここで助けてくれることを願っています。このアプリケーションは、3秒ごとにメッセージテーブルをポーリングして、送信する通知を探します。これは、1つを除くすべてのお客様(シングルテナントDB)でうまく機能します。 1日23時間アクティビティがなく、数千のメッセージを一度にロードします(3000以上)。その他の場合、このボリュームは何もないため、簡単に対処できますが、この場合を除き、以下のSQLクエリの実行には約30秒かかり、更新時にキューがバックアップされ、排他ロックが必要で、したがって、他のすべてのクエリをブロックするため、問題はあらゆる種類の混乱を引き起こします。これはすべて、不適切なクエリプランが原因です。

毎日午前5時に実行される毎日の再インデックス(再編成<30%、再構築> 30%、無視<5%)と統計の更新があります。これらは両方ともOla Hallengrenメンテナンスソリューションからのものです。また、SQL Server 2016を使用しており、完全に最新の状態です(13.0.5492.2)

私は2つのプランが手元にないのですが、基本的に悪いプランは、MessagesSentテーブル(3.5m行)の全テーブルスキャンを実行します。

私の理論では、クエリは1日中何も返さないため、特定の部分は実行されず、したがって、不正なクエリはSQLの最も効率的なクエリです。

同じプランが生成されるだけなので、クエリのプランをフラッシュした後もこれは続きますが、MessagesSentテーブルで統計を更新すると、適切なプランが作成され、すべてが正常で、クエリは約10〜30ミリ秒で実行されます。

クエリが返すデータが存在しない場合でも、常により良い計画を使用するようにこれを微調整する方法を誰かが知っていますか?修正プログラムとして、オプションの再コンパイルをアプリケーションに追加しましたが、これが3秒ごとに実行されるクエリの理想的な解決策だとは思いません。

これがクエリです:

WITH TopMessage
    AS
    (
        SELECT TOP 1 ID, BatchID FROM MessagesSent 
        JOIN Units ON Unit = idUnit 
        WHERE   MessageDate <= GETDATE() 
          AND         Active = 'True' 
          AND         Status = 'Queued' 
          AND NOT(DialString = 'null') 
          AND           Unit = ('29') 
          AND System in ('SystemName', 'Q1', '') 
        ORDER BY 
            CASE 
                WHEN QPriority IS NULL 
                    THEN 
                        CASE 
                            WHEN DefaultPriority IS NULL 
                                THEN 999999 
                                ELSE DefaultPriority 
                        END
                 ELSE QPriority 
            END ASC,
            Retries ASC, 
            MessageDate ASC, 
            ID ASC
    ),
    BatchCalls
    AS
    (
        SELECT * FROM MessagesSent 
        WHERE (
                 (LEN(BatchID) > 0 
                  AND BatchID = (SELECT TOP 1 BatchID FROM TopMessage)
                 ) 
        OR ID = (SELECT TOP 1 ID FROM TopMessage)
        )
        AND Status = 'Queued' AND Active = 'True'
    )

    UPDATE BatchCalls
    SET LastUpdated = @dtNow
    OUTPUT INSERTED.*
    WHERE Status = 'Queued'

お時間を割いていただき、ありがとうございました。

4
WadeH

手動 計画ガイド を使用して、目的の計画を実施できます。

または、GUIを介して クエリストアを実行するクエリストア を使用することもできます。

また、メッセージが大量にロードされた後に統計更新ジョブを実行することもできます。 簡単な方法 を示す投稿をブログに書きました。

9
Max Vernon

これらのクエリの間にはいくつかの共通のフィルタリングがあるため、「アクティブ」列と「ステータス」列にフィルタリングされたインデックスを追加することで、処理を高速化し、ロック/ブロックの量を減らすことができる場合があります。

スキーマがないと、どの列がどのテーブルに属しているかを識別するのは困難ですが、インデックスは次のようになります。

CREATE NONCLUSTERED INDEX IX_BatchID_Filtered
ON dbo.MessagesSent (BatchID, Active, Status)
WHERE Active = 'True' AND Status = 'Queued';

注:BatchIDよりも優れた先行列がある場合があり、テーブルから他の列(DialString、System、QPriorityなど)を含めることができます)-どの列がどのテーブルに属していたのか、そして、それらのデータ型は何であるかなど

このインデックスには、これらの基準を満たす行の(できれば小さい)サブセットのみが含まれ、多少古い統計に直面してもより予測可能なパフォーマンスが提供されます。

6
Josh Darnell