私はこのようなテーブルを持っています:
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
重複するUpdateId
sを削除するために、ObjectId
のインデックスシークとflow個別を使用して最適なプランを生成する方法を見つけようとしています。何か案は?
サンプルデータ 必要な場合。オブジェクトが複数の更新を実行することはめったになく、100行のセット内に複数の更新が存在することはほとんどありません。そのため、何かがない限り、フローを区別するよくわからない?ただし、単一のObjectId
がテーブル内の行数が100を超えないという保証はありません。テーブルには1,000,000を超える行があり、急速に拡大することが予想されます。
これのユーザーが適切な次の@fromUpdateId
を見つける別の方法を持っていると想定します。このクエリで返す必要はありません。
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));
推定された計画は、インデックスシークとフローが明確に異なることを示しています。
出力は確かに次のように始まるように注文されているようです:
...しかし、さらに下の値は「欠落」し始めます:
...そして最終的に:
この特定のケースでの説明は、ハッシュ演算子が流出するということです。
パーティションが流出すると、同じパーティションにハッシュされるすべての行も流出します。こぼれたパーティションは後で処理されるため、遭遇した個別の値は、受け取った順序ですぐに出力されるという期待に反します。
再帰やカーソルの使用など、必要な順序付けされた結果を生成する効率的なクエリを作成する方法はたくさんあります。ただし、Hash Match Flow Distinctを使用して行うことはできません。
正しいことが保証された結果とともにフローの個別の演算子を取得できなかったため、この答えには満足していません。ただし、正しい結果とともに良好なパフォーマンスが得られる代替手段があります。残念ながら、テーブルに非クラスター化インデックスを作成する必要があります。
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;
クエリプラン の画像を次に示します。
最良の場合、SQL Serverは非クラスター化インデックスに対して100回のインデックスシークのみを実行します。非常に不運になるのをシミュレートするために、最初の5000行をクライアントに返すようにクエリを変更しました。その結果、9999のインデックスシークが発生したため、個別のObjectId
ごとに平均100行が取得されます。 SET STATISTICS IO, TIME ON
の出力は次のとおりです。
テーブル「更新」。スキャンカウント10000、論理読み取り31900、物理読み取り0
SQL Server実行時間:CPU時間= 31ミリ秒、経過時間= 42ミリ秒。
私は質問が大好きです-Flow Distinctは私のお気に入りのオペレーターの1つです。
今、保証が問題です。 FDオペレーターがSeekオペレーターから行を順番にプルして、一意であると判断したときに各行を生成することを考えると、正しい順序で行が得られます。しかし、FDが一度に1つの行を処理しないシナリオがあるかどうかを知るのは困難です。
理論的には、FDはSeekから100行を要求し、必要な順序でそれらを生成できます。
クエリヒントOPTION (FAST 1, MAXDOP 1)
が役立つ可能性があります。これは、Seek演算子から必要以上の行を取得することを避けるためです。 保証ですか?結構です。それでも、行のページを一度にプルするか、そのようなものを決定することができます。
OPTION (FAST 1, MAXDOP 1)
を使用すると、OFFSET
バージョンは注文についてconfidenceの多くを提供しますが、保証ではありません。