WHERE INを使用した削除操作中の予期しないスキャン


40

次のようなクエリがあります。

DELETE FROM tblFEStatsBrowsers WHERE BrowserID NOT IN (
    SELECT DISTINCT BrowserID FROM tblFEStatsPaperHits WITH (NOLOCK) WHERE BrowserID IS NOT NULL
)

tblFEStatsBrowsersには553行あります。
tblFEStatsPaperHitsには47.974.301行があります。

tblFEStatsBrowsers:

CREATE TABLE [dbo].[tblFEStatsBrowsers](
    [BrowserID] [smallint] IDENTITY(1,1) NOT NULL,
    [Browser] [varchar](50) NOT NULL,
    [Name] [varchar](40) NOT NULL,
    [Version] [varchar](10) NOT NULL,
    CONSTRAINT [PK_tblFEStatsBrowsers] PRIMARY KEY CLUSTERED ([BrowserID] ASC)
)

tblFEStatsPaperHits:

CREATE TABLE [dbo].[tblFEStatsPaperHits](
    [PaperID] [int] NOT NULL,
    [Created] [smalldatetime] NOT NULL,
    [IP] [binary](4) NULL,
    [PlatformID] [tinyint] NULL,
    [BrowserID] [smallint] NULL,
    [ReferrerID] [int] NULL,
    [UserLanguage] [char](2) NULL
)

BrowserIDを含まないtblFEStatsPaperHitsにはクラスター化インデックスがあります。したがって、内部クエリを実行するには、tblFEStatsPaperHitsの全テーブルスキャンが必要になります。これはまったく問題ありません。

現在、tblFEStatsBrowsersの各行に対してフルスキャンが実行されています。つまり、tblFEStatsPaperHitsの553のフルテーブルスキャンがあります。

WHERE EXISTSに書き換えても計画は変わりません。

DELETE FROM tblFEStatsBrowsers WHERE NOT EXISTS (
    SELECT * FROM tblFEStatsPaperHits WITH (NOLOCK) WHERE BrowserID = tblFEStatsBrowsers.BrowserID
)

ただし、Adam Machanicが示唆しているように、HASH JOINオプションを追加すると、最適な実行計画(tblFEStatsPaperHitsの1回のスキャンのみ)になります。

DELETE FROM tblFEStatsBrowsers WHERE NOT EXISTS (
    SELECT * FROM tblFEStatsPaperHits WITH (NOLOCK) WHERE BrowserID = tblFEStatsBrowsers.BrowserID
) OPTION (HASH JOIN)

これは、これをどのように修正するかという問題ではありません。OPTION(HASH JOIN)を使用するか、一時テーブルを手動で作成できます。クエリオプティマイザーが現在実行しているプラ​​ンを使用するのはなぜだろうと思います。

QOのBrowserID列には統計情報がないため、最悪の場合-5000万の異なる値を想定しているため、かなり大きなメモリ内/ tempdbワークテーブルが必要です。そのため、最も安全な方法は、tblFEStatsBrowsersで各行のスキャンを実行することです。2つのテーブルのBrowserID列間に外部キー関係はないため、QOはtblFEStatsBrowsersから情報を差し引くことができません。

これは、見た目と同じくらい単純なのでしょうか?

更新1
いくつかの統計情報を提供するには:OPTION(HASH JOIN):
208.711論理読み取り(12スキャン)

オプション(ループ結合、
ハッシュグループ):11.008.698論理読み取り(BrowserID(339)ごとにスキャン)

オプションなし:
11.008.775論理読み取り(〜BrowserID(339)ごとにスキャン)

更新2
すばらしい回答、皆さん-ありがとう!1つだけを選ぶのは難しい。マーティンが最初であり、レムスは優れたソリューションを提供しますが、詳細をメンタルに伝えるためにキウイに提供する必要があります:)


5
あるサーバーから別のサーバーに統計をコピーして、複製できるように、統計をスクリプト化できますか?
マークストーリースミス

2
MarkStorey-スミス確かに@ - pastebin.com/9HHRPFgKは、 あなたが空のデータベースでスクリプトを実行すると仮定すると、これは、実行計画を示すなどの際に問題のあるクエリを再現するために私を可能にします。両方のクエリは、スクリプトの最後に含まれています。
マークS.ラスムッセン

回答:


61

「クエリオプティマイザーが現在実行しているプラ​​ンを使用するのはなぜだろうか」

別の言い方をすれば、問題は、次の計画が(多数ある)代替案と比較してオプティマイザーに最も安く見える理由です。

元の計画

結合の内側は、本質的に、の相関値ごとに次の形式のクエリを実行していますBrowserID

DECLARE @BrowserID smallint;

SELECT 
    tfsph.BrowserID 
FROM dbo.tblFEStatsPaperHits AS tfsph 
WHERE 
    tfsph.BrowserID = @BrowserID 
OPTION (MAXDOP 1);

紙当たりスキャン

行数の推定値であることに注意してください185220(ない289013等価比較暗黙的に除外以降)NULL(しない限りANSI_NULLSですOFF)。上記のプランの推定コストは206.8ユニットです。

TOP (1)句を追加しましょう:

DECLARE @BrowserID smallint;

SELECT TOP (1)
    tfsph.BrowserID 
FROM dbo.tblFEStatsPaperHits AS tfsph 
WHERE 
    tfsph.BrowserID = @BrowserID 
OPTION (MAXDOP 1);

TOP付き(1)

現在、推定コストは0.00452ユニットです。Top物理演算子を追加すると、Top演算子の行目標が1行に設定されます。問題は、クラスタ化インデックススキャンの「行の目標」を導き出す方法になります。つまり、1つの行がBrowserID述部と一致するまでにスキャンが処理する必要がある行数は何ですか?

使用可能な統計情報には、166の異なるBrowserID値が表示されます(1 / [すべての密度] = 1 / 0.006024096 = 166)。Costingでは、個別の値が物理行に均一に分散されることを前提としているため、クラスター化インデックススキャンの行目標は166.302に設定されます(サンプリング統計が収集されてからのテーブルカーディナリティの変化を考慮)。

予想される166行をスキャンするための推定コストはそれほど大きくありません(339回、各変更ごとに1回実行されますBrowserID)-クラスター化インデックススキャンは、1.3219ユニットの推定コストを示し、行ゴールのスケーリング効果を示します。I / OおよびCPUのスケールなしのオペレーターコストは、それぞれ153.931および52.8698として示されています。

行目標のスケーリングされた推定コスト

実際には、インデックスからスキャンされた最初の166行(返される順序に関係なく)に可能性のある値が1つずつ含まれることはほとんどありませんBrowserID。それでも、DELETEプランの合計コストは1.40921ユニットであり、そのためにオプティマイザーによって選択されます。Bart Duncanは、このタイプの別の例を、Row Goals Gone Rogueという最近の投稿で示しています。

また、実行計画のTop演算子が反準結合に関連付けられていないことに注意することも興味深いです(特に、「短絡」マーティンが言及しています)。GbAggToConstScanOrTopと呼ばれる探索ルールを最初に無効にすることで、Topの由来を確認できます

DBCC RULEOFF ('GbAggToConstScanOrTop');
GO
DELETE FROM tblFEStatsBrowsers 
WHERE BrowserID NOT IN 
(
    SELECT DISTINCT BrowserID 
    FROM tblFEStatsPaperHits WITH (NOLOCK) 
    WHERE BrowserID IS NOT NULL
) OPTION (MAXDOP 1, LOOP JOIN, RECOMPILE);
GO
DBCC RULEON ('GbAggToConstScanOrTop');

GbAggToConstScanOrTop無効

この計画の推定コストは364.912であり、TopがGroup By Aggregateを置き換えたことを示しています(相関列によるグループ化BrowserID)。集計はクエリテキストの冗長性によるものではありませんDISTINCT。2つの探索ルールLASJNtoLASJNonDistLASJOnLclDistによって導入できる最適化です。これら2つを無効にすると、この計画も作成されます。

DBCC RULEOFF ('LASJNtoLASJNonDist');
DBCC RULEOFF ('LASJOnLclDist');
DBCC RULEOFF ('GbAggToConstScanOrTop');
GO
DELETE FROM tblFEStatsBrowsers 
WHERE BrowserID NOT IN 
(
    SELECT DISTINCT BrowserID 
    FROM tblFEStatsPaperHits WITH (NOLOCK) 
    WHERE BrowserID IS NOT NULL
) OPTION (MAXDOP 1, LOOP JOIN, RECOMPILE);
GO
DBCC RULEON ('LASJNtoLASJNonDist');
DBCC RULEON ('LASJOnLclDist');
DBCC RULEON ('GbAggToConstScanOrTop');

スプール計画

この計画の推定コストは40729.3ユニットです。

Group ByからTopへの変換を行わない場合、オプティマイザーBrowserIDは反自然結合の前に集約を伴うハッシュ結合プランを「自然に」選択します。

DBCC RULEOFF ('GbAggToConstScanOrTop');
GO
DELETE FROM tblFEStatsBrowsers 
WHERE BrowserID NOT IN 
(
    SELECT DISTINCT BrowserID 
    FROM tblFEStatsPaperHits WITH (NOLOCK) 
    WHERE BrowserID IS NOT NULL
) OPTION (MAXDOP 1, RECOMPILE);
GO
DBCC RULEON ('GbAggToConstScanOrTop');

トップDOP 1プランなし

また、MAXDOP 1の制限なしで、並列プランは次のようになります。

上位並列計画なし

元のクエリを「修正」する別の方法BrowserIDは、実行計画が報告する欠落インデックスを作成することです。ネストされたループは、内側にインデックスが付けられている場合に最適です。セミジョインのカーディナリティの推定は、最高の状態では困難です。適切なインデックスを持たない(大きなテーブルには一意のキーさえない!)ことはまったく役に立ちません。

これについては、行の目標、パート4:アンチ結合アンチパターンで詳しく説明しました。


3
あなたに私はお辞儀をします。あなたは私がこれまでに出会ったことのないいくつかの新しい概念を紹介してくれました。何かを知っていると感じたら、誰かがあなたを落ち込ませます-良い方法で:)インデックスを追加すると間違いなく役立ちます。ただし、この1回限りの操作に加えて、フィールドはBrowserID列によってアクセス/集計されることはないため、テーブルが非常に大きいため(これらは多くの同一データベースの1つです)、これらのバイトを保存したいです。テーブルには固有の一意性がないため、テーブルには一意のキーはありません。すべての選択は、PaperIDおよびオプションでピリオドによって行われます。
マークS.ラスムッセン

22

スクリプトを実行して統計のみのデータベースと質問のクエリを作成すると、次の計画が得られます。

計画

計画に示されている表の基数は

  • tblFEStatsPaperHits48063400
  • tblFEStatsBrowsers339

そのため、tblFEStatsPaperHits339回スキャンを実行する必要があると推定しています。各スキャンには、tblFEStatsBrowsers.BrowserID=tblFEStatsPaperHits.BrowserID AND tblFEStatsPaperHits.BrowserID IS NOT NULLスキャン演算子にプッシュダウンされる相関述語があります。

ただし、計画では339のフルスキャンが行われることを意味しません。各スキャンで最初に一致する行が見つかるとすぐに反準結合演算子の下にあるため、残りの行を短絡できます。このノードの推定サブツリーコストはで1.32603あり、プラン全体のコストは1.41337です。

ハッシュ結合の場合、以下のプランが提供されます

ハッシュ結合

プラン全体のコストは418.415(ネストループプランの約300倍)で、単一の完全なクラスター化インデックススキャンtblFEStatsPaperHits206.8単独でコストがかかります。これを1.32603以前に与えられた339の部分スキャンの推定値と比較します(平均部分スキャンの推定コスト= 0.003911592)。

したがって、これは各部分スキャンのコストがフルスキャンよりも53,000倍低いことを示しています。コスト計算が行数に比例してスケーリングする場合、一致する行を見つけて短絡する前に、各反復で平均900行のみを処理する必要があると想定されることを意味します。

しかし、原価計算がその線形の方法でスケーリングするとは思わない。固定起動コストの要素も組み込まれていると思います。TOP次のクエリでさまざまな値を試します

SELECT TOP 147 BrowserID 
FROM [dbo].[tblFEStatsPaperHits] 

147最も近い推定サブツリーコストを与える0.0039115920.0039113。いずれにせよ、各スキャンが処理するのは表のごく一部、数百万行ではなく数百行程度であるという前提に基づいてコスト計算を行っていることは明らかです。

この仮定がどの数学に基づいているのか正確にはわからず、残りの計画の行カウントの推定値と実際には加算されません(ネストされたループ結合から出てくる236の推定行は、236一致する行がまったく見つからず、フルスキャンが必要な場合)。これは、モデリングの前提が多少落ち、ネストされたループの計画が大幅にコストをかけられたままになっているだけのケースだと思います。


20

私の本では、5,000万行の1回のスキャンも受け入れられません...私の通常のトリックは、個別の値を具体化し、それを最新の状態に保ちながらエンジンを委任することです。

create view [dbo].[vwFEStatsPaperHitsBrowserID]
with schemabinding
as
select BrowserID, COUNT_BIG(*) as big_count
from [dbo].[tblFEStatsPaperHits]
group by [BrowserID];
go

create unique clustered index [cdxVwFEStatsPaperHitsBrowserID] 
  on [vwFEStatsPaperHitsBrowserID]([BrowserID]);
go

これにより、BrowserIDごとに1行のマテリアライズドインデックスが提供され、50M行をスキャンする必要がなくなります。エンジンはあなたのためにそれを維持し、QOはあなたが投稿したステートメントで「現状のまま」使用します(ヒントやクエリの書き換えなし)。

欠点はもちろん競合です。での挿入または削除操作tblFEStatsPaperHits(および大量の挿入を含むログテーブル)は、指定されたBrowserIDへのアクセスをシリアル化する必要があります。喜んで購入するなら、これを実行可能にする方法(遅延更新、2段階のログ記録など)があります。


私はあなたに聞いた、そのような大きなスキャンは一般的に間違いなく一般に受け入れられない。この場合、1回限りのデータクリーンアップ操作のために、追加のインデックスを作成しないことを選択しています(システムを中断するため、一時的に作成することはできません)。私はEEを持っていませんが、これが一度限りであることを考えると、ヒントは大丈夫でしょう。私の主な好奇心は、QOがどのように計画を立てたのかということでした:)テーブルはロギングテーブルであり、大量の挿入があります。ただし、tblFEStatsPaperHitsの行を後で更新するため、必要に応じて自分で管理できるように、別の非同期ログテーブルがあります。
マークS.ラスムッセン
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.