SELECT DISTINCT TOP Nクエリがテーブル全体をスキャンするのはなぜですか?


27

SELECT DISTINCT TOP NSQL Serverクエリオプティマイザーによる最適化が不十分と思われるいくつかのクエリに遭遇しました。些細な例を考えてみましょう。2つの交互の値を持つ100万行のテーブルです。私が使用しますGetNumsのデータを生成する機能を:

DROP TABLE IF EXISTS X_2_DISTINCT_VALUES;

CREATE TABLE X_2_DISTINCT_VALUES (PK INT IDENTITY (1, 1), VAL INT NOT NULL);

INSERT INTO X_2_DISTINCT_VALUES WITH (TABLOCK) (VAL)
SELECT N % 2
FROM dbo.GetNums(1000000);

UPDATE STATISTICS X_2_DISTINCT_VALUES WITH FULLSCAN;

次のクエリの場合:

SELECT DISTINCT TOP 2 VAL
FROM X_2_DISTINCT_VALUES
OPTION (MAXDOP 1);

SQL Serverは、テーブルの最初のデータページをスキャンするだけで2つの異なる値を見つけることができますが、代わりにすべてのデータをスキャンします。SQL Serverが、要求された数の個別の値を見つけるまでスキャンしないのはなぜですか?

この質問には、ブロックで生成された10個の異なる値を持つ1,000万行を含む次のテストデータを使用してください。

DROP TABLE IF EXISTS X_10_DISTINCT_HEAP;

CREATE TABLE X_10_DISTINCT_HEAP (VAL VARCHAR(10) NOT NULL);

INSERT INTO X_10_DISTINCT_HEAP WITH (TABLOCK)
SELECT REPLICATE(CHAR(65 + (N / 100000 ) % 10 ), 10)
FROM dbo.GetNums(10000000);

UPDATE STATISTICS X_10_DISTINCT_HEAP WITH FULLSCAN;

クラスタ化インデックスを持つテーブルの回答も受け入れられます。

DROP TABLE IF EXISTS X_10_DISTINCT_CI;

CREATE TABLE X_10_DISTINCT_CI (PK INT IDENTITY (1, 1), VAL VARCHAR(10) NOT NULL, PRIMARY KEY (PK));

INSERT INTO X_10_DISTINCT_CI WITH (TABLOCK) (VAL)
SELECT REPLICATE(CHAR(65 + (N / 100000 ) % 10 ), 10)
FROM dbo.GetNums(10000000);

UPDATE STATISTICS X_10_DISTINCT_CI WITH FULLSCAN;

次のクエリは、テーブルから1,000万行すべてをスキャンします。テーブル全体をスキャンしないものを取得するにはどうすればよいですか?SQL Server 2016 SP1を使用しています。

SELECT DISTINCT TOP 10 VAL
FROM X_10_DISTINCT_HEAP
OPTION (MAXDOP 1);

カーソルは10でも動作する可能性があります
パパラッチ

回答:


29

DISTINCT上記のクエリで操作を実行できる3つの異なるオプティマイザルールがあるようです。次のクエリは、リストが網羅的であることを示唆するエラーをスローします。

SELECT DISTINCT TOP 10 ID
FROM X_10_DISTINCT_HEAP
OPTION (MAXDOP 1, QUERYRULEOFF GbAggToSort, QUERYRULEOFF GbAggToHS, QUERYRULEOFF GbAggToStrm);

メッセージ8622、レベル16、状態1、行1

このクエリで定義されたヒントのため、クエリプロセッサはクエリプランを作成できませんでした。ヒントを指定せずに、SET FORCEPLANを使用せずに、クエリを再送信します。

GbAggToSortgroup-by集計(個別)を個別のソートとして実装します。これは、行を生成する前に入力からすべてのデータを読み取るブロッキング演算子です。GbAggToStrmgroup-by集約をストリーム集約として実装します(このインスタンスでは入力ソートも必要です)。これもブロッキング演算子です。GbAggToHSハッシュマッチとして実装します。これは、質問の悪い計画で見たものですが、ハッシュマッチ(集約)またはハッシュマッチ(フロー別)として実装できます。

ハッシュ一致(flow distinct)演算子は、ブロッキングではないため、この問題を解決する1つの方法です。SQL Serverは、十分な個別値が見つかったらスキャンを停止できる必要があります。

Flow Distinct論理演算子は入力をスキャンし、重複を削除します。Distinctオペレーターは、出力を生成する前にすべての入力を消費しますが、Flow Distinctオペレーターは、入力から取得された各行を返します(その行が重複していない場合は破棄されます)。

質問のクエリでハッシュ一致(フロー別)ではなくハッシュ一致(集計)が使用されるのはなぜですか?テーブル内の個別の値の数が変化すると、テーブルにスキャンする必要がある行数の推定値が減少するため、ハッシュマッチ(フロー個別)クエリのコストが減少すると予想されます。作成する必要があるハッシュテーブルが大きくなるため、ハッシュマッチ(集計)プランのコストが増加すると予想されます。これを調査する1つの方法は、計画ガイドを作成することです。データのコピーを2つ作成し、そのうちの1つにプランガイドを適用すると、同じデータに対してハッシュマッチ(集計)とハッシュマッチ(個別)を並べて比較できるはずです。同じルールが両方のプランに適用されるため、クエリオプティマイザールールを無効にしてこれを実行できないことに注意してください(GbAggToHS)。

計画ガイドを取得する1つの方法を次に示します。

DROP TABLE IF EXISTS X_PLAN_GUIDE_TARGET;

CREATE TABLE X_PLAN_GUIDE_TARGET (VAL VARCHAR(10) NOT NULL);

INSERT INTO X_PLAN_GUIDE_TARGET WITH (TABLOCK)
SELECT CAST(N % 10000 AS VARCHAR(10))
FROM dbo.GetNums(10000000);

UPDATE STATISTICS X_PLAN_GUIDE_TARGET WITH FULLSCAN;

-- run this query
SELECT DISTINCT TOP 10 VAL  FROM X_PLAN_GUIDE_TARGET  OPTION (MAXDOP 1)

プランハンドルを取得し、それを使用してプランガイドを作成します。

-- plan handle is 0x060007009014BC025097E88F6C01000001000000000000000000000000000000000000000000000000000000
SELECT qs.plan_handle, st.text FROM 
sys.dm_exec_query_stats AS qs   
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) AS st  
WHERE st.text LIKE '%X[_]PLAN[_]GUIDE[_]TARGET%'
ORDER BY last_execution_time DESC;

EXEC sp_create_plan_guide_from_handle 
'EVIL_PLAN_GUIDE', 
0x060007009014BC025097E88F6C01000001000000000000000000000000000000000000000000000000000000;

プランガイドは正確なクエリテキストでのみ機能するため、プランガイドからコピーして戻します。

SELECT query_text
FROM sys.plan_guides
WHERE name = 'EVIL_PLAN_GUIDE';

データをリセットします。

TRUNCATE TABLE X_PLAN_GUIDE_TARGET;

INSERT INTO X_PLAN_GUIDE_TARGET WITH (TABLOCK)
SELECT REPLICATE(CHAR(65 + (N / 100000 ) % 10 ), 10)
FROM dbo.GetNums(10000000);

プランガイドを適用したクエリのクエリプランを取得します

SELECT DISTINCT TOP 10 VAL  FROM X_PLAN_GUIDE_TARGET  OPTION (MAXDOP 1)

これには、テストデータで必要なハッシュ一致(フロー別)演算子があります。SQL Serverはテーブルからすべての行を読み取ることを想定しており、推定コストはハッシュ一致(集計)のプランとまったく同じであることに注意してください。私が行ったテストでは、計画の行の目標がSQL Serverがテーブルから期待する個別の値の数以上である場合、2つの計画のコストは同一であることが示唆されました。統計学。残念ながら(このクエリでは)コストが同じ場合、オプティマイザーはハッシュマッチ(集計)からハッシュマッチ(フロー別)を選択します。したがって、我々は望んでいる計画から0.0000001のマジックオプティマイザーユニットから離れています。

この問題を攻撃する1つの方法は、行の目標を減らすことです。ビューの観点からの行の目標がオプティマイザーである場合、行の個別のカウントよりも少ない場合、おそらくハッシュ一致(フローは個別)になります。これは、OPTIMIZE FORクエリヒントを使用して実行できます。

DECLARE @j INT = 10;

SELECT DISTINCT TOP (@j) VAL
FROM X_10_DISTINCT_HEAP
OPTION (MAXDOP 1, OPTIMIZE FOR (@j = 1));

このクエリの場合、オプティマイザは、クエリが最初の行のみを必要とするかのようにプランを作成しますが、クエリが実行されると、最初の10行が返されます。私のマシンでは、このクエリは892800行をスキャンX_10_DISTINCT_HEAPし、299ミリ秒で完了し、250ミリ秒のCPU時間と2537の論理読み取りを行います。

統計が1つの異なる値のみを報告する場合、この手法は機能しないことに注意してください。これは、歪んだデータに対するサンプリングされた統計で発生する可能性があります。ただし、その場合、このような手法を使用して正当化するのに十分なほどデータが密集しているとは考えられません。特に並行して実行できる場合は、テーブル内のすべてのデータをスキャンしても多くの損失はありません。

この問題を攻撃するもう1つの方法は、SQL Serverがベーステーブルから取得することを期待する推定個別値の数を増やすことです。これは予想よりも困難でした。決定論的関数を適用しても、結果の明確なカウントを増やすことはできません。クエリオプティマイザーがその数学的な事実を認識している場合(少なくともテストの目的であることが示唆されています)、確定的な関数(すべての文字列関数含む)を適用しても、個別の行の推定数は増加しません。

NEWID()およびの明らかな選択を含め、非決定的関数の多くも機能しませんでしたRAND()。ただし、LAG()このクエリのトリックは行います。クエリオプティマイザーは、LAG式に対して1,000万の個別の値を期待しているため、ハッシュマッチ(フロー個別)プランが促進されます。

SELECT DISTINCT TOP 10 LAG(VAL, 0) OVER (ORDER BY (SELECT NULL)) AS ID
FROM X_10_DISTINCT_HEAP
OPTION (MAXDOP 1);

私のマシンでは、このクエリは892800行をスキャンX_10_DISTINCT_HEAPし、1165ミリ秒で1109ミリ秒のCPU時間と2537の論理読み取りで完了するため、LAG()かなりのオーバーヘッドが追加されます。@Paul Whiteは、このクエリに対してバッチモード処理を試みることを提案しました。SQL Server 2016では、でもバッチモード処理を取得できますMAXDOP 1。行ストアテーブルのバッチモード処理を取得する1つの方法は、次のように空のCCIに結合することです。

CREATE TABLE #X_DUMMY_CCI (ID INT NOT NULL);

CREATE CLUSTERED COLUMNSTORE INDEX X_DUMMY_CCI ON #X_DUMMY_CCI;

SELECT DISTINCT TOP 10 VAL
FROM
(
    SELECT LAG(VAL, 1) OVER (ORDER BY (SELECT NULL)) AS VAL
    FROM X_10_DISTINCT_HEAP
    LEFT OUTER JOIN #X_DUMMY_CCI ON 1 = 0
) t
WHERE t.VAL IS NOT NULL
OPTION (MAXDOP 1);

このコードにより、このクエリプランが作成されます。

Paulは、Window Aggregate最適化の対象とは思えないLAG(..., 1)ため、使用するクエリを変更する必要があることを指摘しましたLAG(..., 0)。この変更により、経過時間が520ミリ秒に、CPU時間が454ミリ秒に短縮されました。

このLAG()アプローチは最も安定したものではないことに注意してください。Microsoftが関数に対する一意性の仮定を変更すると、機能しなくなる可能性があります。レガシーCEとは異なる見積もりがあります。また、ヒープに対するこのタイプの最適化は、良いアイデアではありません。テーブルが再構築されると、ほとんどすべての行をテーブルから読み取る必要がある最悪のシナリオに陥ることがあります。

一意の列を持つテーブル(質問のクラスター化インデックスの例など)に対して、より良いオプションがあります。たとえばSUBSTRING、常に空の文字列を返す式を使用して、オプティマイザーをだますことができます。SQL ServerはSUBSTRING、個別の値の数が変更されるとは考えていないため、PKなどの一意の列に適用すると、個別の行の推定数は1,000万になります。次のクエリは、ハッシュ一致(フロー個別)演算子を取得します

SELECT DISTINCT TOP 10 VAL + SUBSTRING(CAST(PK AS VARCHAR(10)), 11, 1)
FROM X_10_DISTINCT_CI
OPTION (MAXDOP 1);

私のマシンでは、このクエリX_10_DISTINCT_CIは333ミリ秒で900000行をスキャンし、297ミリ秒のCPU時間と3011の論理読み取りで完了します。

要約すると、クエリオプティマイザーは、テーブルの推定個別行数が> =であるSELECT DISTINCT TOP N場合、クエリのテーブルからすべての行が読み取られると想定しているように見えNます。ハッシュ一致(集約)演算子は、ハッシュ一致(フロー個別)演算子と同じコストを持つ場合がありますが、オプティマイザーは常に集約演算子を選択します。これにより、十分な個別の値がテーブルスキャンの開始近くにある場合、不要な論理読み取りが発生する可能性があります。ハッシュマッチ(フロー別)演算子を使用するようにオプティマイザーをだます2つの方法は、OPTIMIZE FORヒントを使用して行の目標を下げるLAG()SUBSTRING、一意の列を使用してまたは一意の列で個別の行の推定数を増やすことです。


12

あなたはすでにあなた自身の質問に正しく答えています。

私は、最も効率的な方法は実際にテーブル全体をスキャンすることであるという観察結果を追加したいだけです- 列ストア「ヒープ」として整理できる場合:

CREATE CLUSTERED COLUMNSTORE INDEX CCSI 
ON dbo.X_10_DISTINCT_HEAP;

単純なクエリ:

SELECT DISTINCT TOP (10)
    XDH.VAL 
FROM dbo.X_10_DISTINCT_HEAP AS XDH
OPTION (MAXDOP 1);

次に与える:

実行計画

テーブル 'X_10_DISTINCT_HEAP'。スキャン数1
 論理読み取り0、物理読み取り0、先読み読み取り0、 
 lob論理読み取り66、lob物理読み取り0、lob先読み読み取り0。
テーブル 'X_10_DISTINCT_HEAP'。セグメントは13を読み取り、セグメントは0をスキップしました。

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

ハッシュマッチ(Flow Distinct)は現在、バッチモードで実行できません。これを使用する方法は、バッチ処理から行処理への(目に見えない)高価な移行のため、はるかに遅くなります。例えば:

SET ROWCOUNT 10;

SELECT DISTINCT 
    XDH.VAL
FROM dbo.X_10_DISTINCT_HEAP AS XDH
OPTION (FAST 1);

SET ROWCOUNT 0;

与える:

フロー個別実行計画

テーブル 'X_10_DISTINCT_HEAP'。スキャン数1
 論理読み取り0、物理読み取り0、先読み読み取り0、 
 lob論理読み取り20、lob物理読み取り0、lob先読み読み取り0。
テーブル 'X_10_DISTINCT_HEAP'。セグメントは4を読み取り、セグメントは0をスキップしました。

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

これは、テーブルが行ストアヒープとして編成されている場合よりも遅くなります。


4

再帰CTEを使用して、繰り返される部分スキャン(スキップスキャンに似ていますが、同じではありません)をエミュレートする試みがあります。目的は-インデックスがない(id)ため-テーブルでのソートと複数のスキャンを回避することです。

いくつかの再帰的なCTE制限を回避するためのトリックを実行します。

  • TOP再帰部分では許可されていません。ROW_NUMBER()代わりにサブクエリを使用します。
  • 定数部分への複数の参照、LEFT JOINまたはNOT IN (SELECT id FROM cte)再帰部分からの使用または使用はできません。バイパスするために、VARCHARすべてのid値を蓄積する文字列を構築します。これSTRING_AGGは、hierarchyIDと類似しているか、hierarchyIDと類似していますLIKE

rextester.comのヒープ(列の名前が仮定されている場合idtest-1の場合

これは、テストで示されているように、複数のスキャンを回避しませんが、最初の数ページで異なる値が見つかった場合にOKを実行します。ただし、値が均等に分散されていない場合、テーブルの大部分で複数のスキャンが実行される可能性があります。これにより、パフォーマンスが低下します。

WITH ct (id, found, list) AS
  ( SELECT TOP (1) id, 1, CAST('/' + id + '/' AS VARCHAR(MAX))
    FROM x_large_table_2
  UNION ALL
    SELECT y.ID, ct.found + 1, CAST(ct.list + y.id + '/' AS VARCHAR(MAX))
    FROM ct
      CROSS APPLY 
      ( SELECT x.id, 
               rn = ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
        FROM x_large_table_2 AS x
        WHERE ct.list NOT LIKE '%/' + id + '/%'
      ) AS y
    WHERE ct.found < 3         -- the TOP (n) parameter here
      AND y.rn = 1
  )
SELECT id FROM ct ;

テーブルがクラスター化されている場合(CI on unique_key)、rextester.comのtest-2

これは、クラスター化インデックス(WHERE x.unique_key > ct.unique_key)を使用して、複数のスキャンを回避します。

WITH ct (unique_key, id, found, list) AS
  ( SELECT TOP (1) unique_key, id, 1, CAST(CONCAT('/',id, '/') AS VARCHAR(MAX))
    FROM x_large_table_2
  UNION ALL
    SELECT y.unique_key, y.ID, ct.found + 1, 
        CAST(CONCAT(ct.list, y.id, '/') AS VARCHAR(MAX))
    FROM ct
      CROSS APPLY 
      ( SELECT x.unique_key, x.id, 
               rn = ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
        FROM x_large_table_2 AS x
        WHERE x.unique_key > ct.unique_key
          AND ct.list NOT LIKE '%/' + id + '/%'
      ) AS y
    WHERE ct.found < 5       -- the TOP (n) parameter here
      AND y.rn = 1
  )
-- SELECT * FROM ct ;        -- for debugging
SELECT id FROM ct ;

このソリューションには、かなり微妙なパフォーマンスの問題があります。N番目の値を見つけた後、テーブルで余分なシークを実行することになります。そのため、上位10個に10個の異なる値がある場合、11番目の値が検索されますが、そこにはありません。最終的に追加のフルスキャンが行われ、1000万のROW_NUMBER()計算が実際に合計されます。私のマシンでクエリを20倍高速化する回避策があります。どう思いますか?brentozar.com/pastetheplan/?id=SkDhAmFKe
ジョーオブビッシュ

2

完全を期すために、この問題に対処する別の方法は、OUTER APPLYを使用することです。OUTER APPLY検索する必要のある個別の値ごとに演算子を追加できます。これは、ypercubeの再帰的アプローチの概念に似ていますが、事実上、再帰が手作業で記述されています。利点の1つTOPは、ROW_NUMBER()回避策の代わりに派生テーブルで使用できることです。大きな欠点の1つは、クエリテキストがN増加するにつれて長くなることです。

ヒープに対するクエリの実装の1つを次に示します。

SELECT VAL
FROM (
    SELECT t1.VAL VAL1, t2.VAL VAL2, t3.VAL VAL3, t4.VAL VAL4, t5.VAL VAL5, t6.VAL VAL6, t7.VAL VAL7, t8.VAL VAL8, t9.VAL VAL9, t10.VAL VAL10
    FROM 
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP 
    ) t1
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t2 WHERE t2.VAL NOT IN (t1.VAL)
    ) t2
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t3 WHERE t3.VAL NOT IN (t1.VAL, t2.VAL)
    ) t3
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t4 WHERE t4.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL)
    ) t4
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t5 WHERE t5.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL)
    ) t5
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t6 WHERE t6.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL)
    ) t6
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t7 WHERE t7.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL)
    ) t7
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t8 WHERE t8.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL, t7.VAL)
    ) t8
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t9 WHERE t9.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL, t7.VAL, t8.VAL)
    ) t9
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t10 WHERE t10.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL, t7.VAL, t8.VAL, t9.VAL)
    ) t10
) t
UNPIVOT 
(
  VAL FOR VALS IN (VAL1, VAL2, VAL3, VAL4, VAL5, VAL6, VAL7, VAL8, VAL9, VAL10)
) AS upvt;

ここでは上記のクエリの実際のクエリプランです。私のマシンでは、このクエリは713ミリ秒で完了し、625ミリ秒のCPU時間と12605の論理読み取りが行われます。10万行ごとに新しい個別の値を取得するので、このクエリは約900000 * 10 * 0.5 = 4500000行をスキャンします。理論上、このクエリは、他の回答からこのクエリの論理読み取りの5倍を実行する必要があります。

DECLARE @j INT = 10;

SELECT DISTINCT TOP (@j) VAL
FROM X_10_DISTINCT_HEAP
OPTION (MAXDOP 1, OPTIMIZE FOR (@j = 1));

そのクエリは2537回の論理読み取りを行いました。2537 * 5 = 12685は12605に非常に近い値です。

クラスター化インデックスを使用したテーブルでは、より良い結果が得られます。これは、最後のクラスター化されたキー値を派生テーブルに渡して、同じ行を2回スキャンすることを回避できるためです。1つの実装:

SELECT VAL
FROM (
    SELECT t1.VAL VAL1, t2.VAL VAL2, t3.VAL VAL3, t4.VAL VAL4, t5.VAL VAL5, t6.VAL VAL6, t7.VAL VAL7, t8.VAL VAL8, t9.VAL VAL9, t10.VAL VAL10
    FROM 
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI 
    ) t1
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t2 WHERE PK > t1.PK AND t2.VAL NOT IN (t1.VAL)
    ) t2
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t3 WHERE PK > t2.PK AND t3.VAL NOT IN (t1.VAL, t2.VAL)
    ) t3
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t4 WHERE PK > t3.PK AND t4.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL)
    ) t4
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t5 WHERE PK > t4.PK AND t5.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL)
    ) t5
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t6 WHERE PK > t5.PK AND t6.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL)
    ) t6
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t7 WHERE PK > t6.PK AND t7.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL)
    ) t7
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t8 WHERE PK > t7.PK AND t8.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL, t7.VAL)
    ) t8
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t9 WHERE PK > t8.PK AND t9.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL, t7.VAL, t8.VAL)
    ) t9
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t10 WHERE PK > t9.PK AND t10.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL, t7.VAL, t8.VAL, t9.VAL)
    ) t10
) t
UNPIVOT 
(
  VAL FOR VALS IN (VAL1, VAL2, VAL3, VAL4, VAL5, VAL6, VAL7, VAL8, VAL9, VAL10)
) AS upvt;

ここでは上記のクエリの実際のクエリプランです。私のマシンでは、このクエリはCPU時間が140ミリ秒、論理読み取りが3203ミリ秒で154ミリ秒で完了します。これはOPTIMIZE FOR、クラスター化インデックステーブルに対するクエリよりも少し高速に実行されるように見えました。私はそれを期待していなかったので、パフォーマンスをより注意深く測定しようとしました。私の方法論は、結果セットせずに各クエリ10時間を実行するから集計の数字を見ていたsys.dm_exec_sessionssys.dm_exec_session_wait_stats。セッション56がAPPLYクエリであり、セッション63がOPTIMIZE FORクエリでした。

出力sys.dm_exec_sessions

╔════════════╦══════════╦════════════════════╦═══════════════╗
 session_id  cpu_time  total_elapsed_time  logical_reads 
╠════════════╬══════════╬════════════════════╬═══════════════╣
         56      1360                1373          32030 
         63      2094                2091          30400 
╚════════════╩══════════╩════════════════════╩═══════════════╝

APPLYクエリのcpu_timeとelapsed_timeには明らかな利点があるようです。

出力sys.dm_exec_session_wait_stats

╔════════════╦════════════════════════════════╦═════════════════════╦══════════════╦══════════════════╦═════════════════════╗
 session_id            wait_type             waiting_tasks_count  wait_time_ms  max_wait_time_ms  signal_wait_time_ms 
╠════════════╬════════════════════════════════╬═════════════════════╬══════════════╬══════════════════╬═════════════════════╣
         56  SOS_SCHEDULER_YIELD                             340             0                 0                    0 
         56  MEMORY_ALLOCATION_EXT                            38             0                 0                    0 
         63  SOS_SCHEDULER_YIELD                             518             0                 0                    0 
         63  MEMORY_ALLOCATION_EXT                            98             0                 0                    0 
         63  RESERVED_MEMORY_ALLOCATION_EXT                  400             0                 0                    0 
╚════════════╩════════════════════════════════╩═════════════════════╩══════════════╩══════════════════╩═════════════════════╝

OPTIMIZE FORクエリは、追加の待機の種類がありますRESERVED_MEMORY_ALLOCATION_EXT。これが何を意味するのか正確にはわかりません。これは、ハッシュマッチ(フロー別)演算子のオーバーヘッドの測定値である可能性があります。いずれにせよ、おそらくCPU時間の70ミリ秒の違いを心配する価値はありません。


1

私はあなたがなぜに答えがあると思う
。これは、それに対処するための方法かもしれないが
、私はそれが乱雑に見えます知っているが、実行計画は、個別のトップ2は、コストの84%であったと述べました

SELECT distinct top (2)  [enumID]
FROM [ENRONbbb].[dbo].[docSVenum1]

declare @table table (enumID tinyint);
declare @enumID tinyint;
set @enumID = (select top (1) [enumID] from [docSVenum1]);
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
select enumID from @table;

私のマシンでは、このコードは5秒かかりました。テーブル変数への結合はかなりのオーバーヘッドを追加するようです。最後のクエリでは、テーブル変数が892800回スキャンされました。このクエリには1359ミリ秒のCPU時間と1374ミリ秒の経過時間がかかりました。予想以上に多かった。主変数をテーブル変数に追加すると役立つようですが、理由はわかりません。他の可能な最適化があるかもしれません。
ジョーオブビッシュ

-4

あなたが何を見ているのかを理解するために、あなたは後ろに立って質問を客観的に見る必要があると思います。

クエリオプティマイザーは、最初に個別の値の完全なリストを特定せずに、上位10個の個別の値を選択できますか?

Select Distinctでは、結果セットを特定するために全テーブル(またはカバーインデックス)スキャンが必要です。考えてみてください-テーブルの最後の行には、これまで見たことのない値が含まれている可能性があります。

Select Distinctは非常に鈍い武器です。


2
あんまり。テーブルをスキャンして、最初の20行に10個の異なる値がある場合、残りのテーブルのスキャンを続行する必要があるのはなぜですか?
ypercubeᵀᴹ

2
10のみを要求するときに、なぜ見続ける必要があるのですか?既に10個の異なる値が見つかっているため、停止する必要があります。それが問題の問題です。
ypercubeᵀᴹ

3
上位N検索で最初に結果セット全体を表示する必要があるのはなぜですか?10個の異なる値があり、それだけが重要な場合は、他の値の検索を停止できます。結果セット全体をソートして最初の10個が別のストーリーであるかどうかを知る必要があるが、どの10を気にせずに10個の異なる値のみが必要な場合、結果セット全体を取得するための論理要件はありません。
トムV-チームモニカ

2
リクエストされたセットを返すというタスクを自分で想像してください。数千万のうち、明確な上位10個の値を指定するように求められ、ソート順に従うように指示されませんでした。たとえば、最初の100個を見て結果に到達した場合、値のセット全体を調べる必要があると感じますか?データベース製品にそのロジックを実装することは別の問題ですが、この問題についてテーブル全体をスキャンすることが論理的に必要であることを示唆しているようですが、そうではありません。
アンドリーM

4
@マルコ:私は同意しません、これ答えです。回答者が質問の前提に同意せず、OPの誤解だと思うものに回答することがあります。
アンドリーM
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.