明確なフローの強制


19

このようなテーブルがあります:

CREATE TABLE Updates
(
    UpdateId INT NOT NULL IDENTITY(1,1) PRIMARY KEY,
    ObjectId INT NOT NULL
)

基本的に、IDが増加するオブジェクトの更新を追跡します。

このテーブルのコンシューマーはUpdateId、特定のから順に特定の100個のオブジェクトIDのチャンクを選択しますUpdateId。基本的に、中断した場所を追跡し、更新をクエリします。

私はクエリのみ書き込むことによって最大限に最適なクエリプランを生成することができましたので、これは興味深い最適化問題であることがわかってきましたが起こる私はインデックスのためにやりたいが、ないが保証する私が欲しいもの:

SELECT DISTINCT TOP 100 ObjectId
FROM Updates
WHERE UpdateId > @fromUpdateId

@fromUpdateIdストアドプロシージャのパラメーターはどこにありますか。

次の計画:

SELECT <- TOP <- Hash match (flow distinct, 100 rows touched) <- Index seek

UpdateId使用されているインデックスのシークにより、結果は既に素晴らしく、必要な更新IDの最低から最高まで並べられています。そして、これはフロー別の計画を生成します。それは私が望むものです。しかし、順序は明らかに動作を保証するものではないため、使用したくありません。

このトリックにより、同じクエリプランが得られます(ただし、冗長なTOPがあります)。

WITH ids AS
(
    SELECT ObjectId
    FROM Updates
    WHERE UpdateId > @fromUpdateId
    ORDER BY UpdateId OFFSET 0 ROWS
)
SELECT DISTINCT TOP 100 ObjectId FROM ids

ただし、これが本当に順序付けを保証するかどうかはわかりません(疑わない)。

SQL Serverが単純化するのに十分スマートであることを期待していたクエリの1つが、非常に悪いクエリプランを生成することになりました。

SELECT TOP 100 ObjectId
FROM Updates
WHERE UpdateId > @fromUpdateId
GROUP BY ObjectId
ORDER BY MIN(UpdateId)

次の計画:

SELECT <- Top N Sort <- Hash Match aggregate (50,000+ rows touched) <- Index Seek

インデックスのシークUpdateIdと重複したs を削除するフローを区別して最適なプランを生成する方法を見つけようとしていますObjectId。何か案は?

必要に応じてサンプルデータ。オブジェクトはめったにつ以上の更新を持っていないだろう、と私は後だ理由である100行のセット内に複数の、持っていることはほとんどないはずです明確な流れをそこの何かが良く、私が知らない場合を除き、?ただし、ObjectId1つのテーブルに100行を超えないという保証はありません。テーブルには1,000,000行を超える行があり、急速に拡大することが予想されます。

これのユーザーが適切な次を見つける別の方法を持っていると仮定してください@fromUpdateId。このクエリで返す必要はありません。

回答:


15

Hash Match Flow Distinct演算子は順序を保持しないため、SQL Serverオプティマイザー、必要な保証が必要な実行計画を作成できません

ただし、これが本当に順序付けを保証するかどうかはわかりません(疑わない)。

多くの場合、順序の維持を観察できますが、これは実装の詳細です。保証はないため、信頼することはできません。いつものように、プレゼンテーションの順序は最上位のORDER BY句によってのみ保証されます。

以下のスクリプトは、Hash Match Flow Distinctが順序を保持しないことを示しています。両方の列に1〜50,000の一致する番号を使用して、対象のテーブルを設定します。

IF OBJECT_ID(N'dbo.Updates', N'U') IS NOT NULL
    DROP TABLE dbo.Updates;
GO
CREATE TABLE Updates
(
    UpdateId INT NOT NULL IDENTITY(1,1),
    ObjectId INT NOT NULL,

    CONSTRAINT PK_Updates_UpdateId PRIMARY KEY (UpdateId)
);
GO
INSERT dbo.Updates (ObjectId)
SELECT TOP (50000)
    ObjectId =
        ROW_NUMBER() OVER (
            ORDER BY C1.[object_id]) 
FROM sys.columns AS C1
CROSS JOIN sys.columns AS C2
ORDER BY
    ObjectId;

テストクエリは次のとおりです。

DECLARE @Rows bigint = 50000;

-- Optimized for 1 row, but will be 50,000 when executed
SELECT DISTINCT TOP (@Rows)
    U.ObjectId 
FROM dbo.Updates AS U
WHERE 
    U.UpdateId > 0
OPTION (OPTIMIZE FOR (@Rows = 1));

推定プランは、インデックスシークとフローが異なることを示しています。

推定計画

出力は確かに次で始まるように順序付けられているようです:

結果の始まり

...しかし、さらに低い値は「欠落」し始めます:

パターン分解

...そして最終的に:

カオスが発生する

この特定の場合の説明は、ハッシュ演算子がこぼれるということです:

実行後計画

パーティションが流出すると、同じパーティションにハッシュするすべての行も流出します。こぼれたパーティションは後で処理され、検出された個別の値が受信された順序ですぐに放出されるという期待を破ります。


再帰やカーソルの使用など、必要な順序付けられた結果を生成する効率的なクエリを作成する方法は多数あります。ただし、Hash Match Flow Distinctを使用して行うことはできません。


11

正しいことが保証された結果とともにフロー別の演算子を取得することができなかったため、この答えには満足していません。ただし、正しい結果とともに良好なパフォーマンスを得る代替手段があります。残念ながら、非クラスター化インデックスをテーブルに作成する必要があります。

この問題に対処するにはORDER BY、可能な列の組み合わせを考え、DISTINCTそれらに適用した後に正しい結果を得るようにしました。最小値UpdateId当たりObjectIdと共にはObjectIdそのような組み合わせです。ただし、最小値を直接要求するとUpdateId、テーブルからすべての行が読み取られるようです。代わりUpdateIdに、テーブルへの別の結合での最小値を間接的に要求できます。このアイデアは、Updatesテーブルを順番にスキャンし、UpdateIdその行のの最小値ではない行ObjectIdを破棄し、最初の100行を保持することです。データ分布の説明に基づいて、非常に多くの行を破棄する必要はありません。

データ準備のために、個別のObjectIdごとに2行のテーブルに100万行を入れました。

INSERT INTO Updates WITH (TABLOCK)
SELECT t.RN / 2
FROM 
(
    SELECT TOP 1000000 -1 + ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) RN
    FROM master..spt_values t1
    CROSS JOIN master..spt_values t2
) t;

CREATE INDEX IX On Updates (Objectid, UpdateId);

上の非クラスタ化インデックスObjectidとはUpdateId重要です。UpdateIdあたりの最小値を持たない行を効率的に破棄できますObjectid。上記の説明に一致するクエリを作成する方法は多数あります。以下がそのような方法の1つNOT EXISTSです。

DECLARE @fromUpdateId INT = 9999;
SELECT ObjectId
FROM (
    SELECT DISTINCT TOP 100 u1.UpdateId, u1.ObjectId
    FROM Updates u1
    WHERE UpdateId > @fromUpdateId
    AND NOT EXISTS (
        SELECT 1
        FROM Updates u2
        WHERE u2.UpdateId > @fromUpdateId
        AND u1.ObjectId = u2.ObjectId
        AND u2.UpdateId < u1.UpdateId
    )
    ORDER BY u1.UpdateId, u1.ObjectId
) t;

クエリプランの写真を次に示します

クエリプラン

最良の場合、SQL Serverは非クラスター化インデックスに対して100回だけインデックスシークを実行します。非常に不運になることをシミュレートするために、最初の5000行をクライアントに返すようにクエリを変更しました。その結果、9999のインデックスシークが発生するため、distinctごとに平均100行を取得するようなものObjectIdです。からの出力はSET STATISTICS IO, TIME ON次のとおりです。

表「更新」。スキャンカウント10000、論理読み取り31900、物理読み取り0

SQL Serverの実行時間:CPU時間= 31ミリ秒、経過時間= 42ミリ秒。


9

私は質問が大好きです-Flow Distinctは私のお気に入りのオペレーターの1つです。

今、保証が問題です。シーク演算子から順序付けられた方法で行をプルし、一意であると判断した各行を生成するFD演算子について考えると、正しい順序で行が得られます。しかし、FDが一度に1つの行を処理しないシナリオがあるかどうかを知ることは困難です。

理論的には、FDはシークから100行を要求し、必要な順序でそれらを生成できます。

クエリヒントOPTION (FAST 1, MAXDOP 1)は、Seek演算子から必要な行より多くの行を取得することを避けるため、役立ちます。それは保証ですか?そうでもない。それでも、一度に1行のページをプルするか、またはそのようなことを決定できます。

OPTION (FAST 1, MAXDOP 1)、あなたのOFFSETバージョンは注文についてあなたに多くの自信を与えると思いますが、それは保証ではありません。


私が理解したように、問題はFlow Distinctオペレーターがディスクに流出する可能性のあるハッシュテーブルを使用することです。スピルがある場合、RAMに残っている部分を使用して処理できる行はすぐに処理されますが、スピルされたデータがディスクから読み戻されるまで他の行は処理されません。私が言えることから、ハッシュテーブル(ハッシュ結合など)を使用する演算子は、その流出動作のために順序を保持することが保証されていません。
sam.bishop

正しい。Paul Whiteの回答をご覧ください。
ロブファーリー
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.