web-dev-qa-db-ja.com

強制フローディスティンクト

私はこのようなテーブルを持っています:

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

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

このテーブルのコンシューマーは、UpdateIdで順序付けされ、特定のUpdateIdから始まる100個の異なるオブジェクトIDのチャンクを選択します。基本的に、中断したところを追跡し、更新を照会します。

これは興味深い最適化問題であることがわかりました。インデックスのためにhappenを実行するクエリを作成することで、最大限に最適なクエリプランを生成できただけだからです。 、しかし私が欲しいものを保証しない

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

重複するUpdateIdsを削除するために、ObjectIdのインデックスシークとflow個別を使用して最適なプランを生成する方法を見つけようとしています。何か案は?

サンプルデータ 必要な場合。オブジェクトが複数の更新を実行することはめったになく、100行のセット内に複数の更新が存在することはほとんどありません。そのため、何かがない限り、フローを区別するよくわからない?ただし、単一のObjectIdがテーブル内の行数が100を超えないという保証はありません。テーブルには1,000,000を超える行があり、急速に拡大することが予想されます。

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

19
Cory Nelson

SQL Serverオプティマイザーはできない必要な保証を備えた後の実行プランを生成できません。これは、ハッシュマッチフローDistinct演算子は順序を保持しません。

ただし、これが本当に注文を保証するものかどうかはわかりません(そうではありません)。

多くの場合、observeオーダー保持を行うことができますが、これは実装の詳細です。保証はありませんので、ご安心ください。いつものように、表示順序はトップレベルのORDER BY句によってのみ保証できます。

以下のスクリプトは、ハッシュマッチフローディスティンクトが順序を保持しないことを示しています。問題のテーブルを設定し、両方の列に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));

推定された計画は、インデックスシークとフローが明確に異なることを示しています。

Estimated plan

出力は確かに次のように始まるように注文されているようです:

Start of results

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

Pattern breaking down

...そして最終的に:

Chaos breaks out

この特定のケースでの説明は、ハッシュ演算子が流出するということです。

Post-execution plan

パーティションが流出すると、同じパーティションにハッシュされるすべての行も流出します。こぼれたパーティションは後で処理されるため、遭遇した個別の値は、受け取った順序ですぐに出力されるという期待に反します。


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

15
Paul White 9

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

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

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

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を持たない行を効率的に破棄できます。上記の説明に一致するクエリを作成するには、多くの方法があります。 NOT EXISTSを使用したそのような方法の1つを次に示します。

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;

クエリプラン の画像を次に示します。

query plan

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

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

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

11
Joe Obbish

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

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

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

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

OPTION (FAST 1, MAXDOP 1)を使用すると、OFFSETバージョンは注文についてconfidenceの多くを提供しますが、保証ではありません。

9
Rob Farley