web-dev-qa-db-ja.com

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

いくつかに遭遇しましたSELECT DISTINCT TOP N SQL 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個の異なる値を持つ1000万行を含む次のテストデータを使用してください。

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;

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

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

上記のクエリで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を使用せずにクエリを再送信します。

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

ハッシュマッチ( フローの区別 )演算子は、ブロッキングではないため、この問題を解決する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行を返します。私のマシンでは、このクエリは_X_10_DISTINCT_HEAP_から892800行をスキャンし、299 msで250 msのCPU時間と2537の論理読み取りで完了します。

統計が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);
_

私のマシンでは、このクエリは_X_10_DISTINCT_HEAP_から892800行をスキャンし、1109 msのCPU時間と2537の論理読み取りで1165 msで完了するため、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);
_

そのコードは this query plan になります。

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

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

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

_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_から900000行をスキャンし、333ミリ秒で297ミリ秒のCPU時間と3011の論理読み取りで完了します。

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

30
Joe Obbish

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

columnstore 'heap' として編成できる場合、最も効率的な方法は実際にテーブル全体をスキャンすることであるという観察を追加したいだけです。

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);

次に与える:

Execution plan

表 'X_10_DISTINCT_HEAP'。スキャンカウント1、
論理読み取り0、物理読み取り0、先読み読み取り0、
 lOB論理読み取り66、ロブ物理読み取り0、ロブ先読み読み取り0。
テーブル 'X_10_DISTINCT_HEAP'。セグメント読み取り13、セグメントスキップ0。
 
 SQL Server実行時間:
 CPU時間= 0 ms、経過時間= 11 ms。

現在、ハッシュマッチ(フローディスティンクト)はバッチモードでは実行できません。これを使用するメソッドは、バッチ処理から行処理への(目に見えない)コストのかかる移行のため、はるかに遅くなります。例えば:

SET ROWCOUNT 10;

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

SET ROWCOUNT 0;

与える:

Flow Distinct Execution Plan

表 'X_10_DISTINCT_HEAP'。スキャンカウント1、
論理読み取り0、物理読み取り0、先読み読み取り0、
 lOB論理読み取り20、ロブ物理読み取り0、ロブ先読み読み取り0。
テーブル 'X_10_DISTINCT_HEAP'。 セグメント読み取り4、セグメントがスキップされました0。
 
 SQL Server実行時間:
 CPU時間= 640 ms、経過時間= 680 ms。

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

12
Paul White 9

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

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

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

ヒープの場合(列の名前がidであると想定) rextester.comのtest-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 ;

テーブルがクラスター化されている場合(unique_keyのCI)、 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 ;
5
ypercubeᵀᴹ

完全を期すために、この問題に取り組む別の方法は OUTER APPLY を使用することです。検索する必要のある個別の値ごとに_OUTER APPLY_演算子を追加できます。これは、概念的にはypercubeの再帰的アプローチと似ていますが、効果的に再帰を手動で記述します。 1つの利点は、ROW_NUMBER()回避策の代わりに派生テーブルでTOPを使用できることです。大きな欠点の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ミリ秒で完了し、CPU時間は625ミリ秒、論理読み取りは12605です。 100k行ごとに新しい個別の値を取得するため、このクエリは約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;
_

ここ は、上記のクエリの実際のクエリプランです。私のマシンでは、このクエリは154ミリ秒で完了し、CPU時間は140ミリ秒、論理読み取りは3203です。これは、クラスター化インデックステーブルに対する_OPTIMIZE FOR_クエリよりも少し高速に実行されるように見えました。期待していなかったので、もっと慎重に性能を測ってみました。私の方法は、結果セットなしで各クエリを10回実行し、_sys.dm_exec_sessions_と_sys.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とlapsed_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ミリ秒の違いを心配する価値はないでしょう。

2
Joe Obbish

なぜだと思いますか
これはそれを解決する方法かもしれません
混乱しているように見えますが、実行計画によると、上位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;
1
paparazzo