web-dev-qa-db-ja.com

複数の列でEXISTSを効率的に確認するにはどうすればよいですか?

これは私が定期的に直面する問題であり、まだ良い解決策を見つけていません。

次のテーブル構造を想定します

_CREATE TABLE T
(
A INT PRIMARY KEY,
B CHAR(1000) NULL,
C CHAR(1000) NULL
)
_

また、要件は、null許容列BまたはCのいずれかに実際にNULL値が含まれているかどうか(および含まれている場合はどちらか)を判別することです。

また、テーブルに数百万行が含まれていると仮定します(このクラスのクエリのより一般的なソリューションに興味があるので、ピークになる可能性のある列統計はありません)。

これに取り組む方法はいくつか考えられますが、すべてに弱点があります。

2つの個別のEXISTSステートメント。これには、NULLが検出されるとすぐにクエリがスキャンを停止できるという利点があります。ただし、実際に両方の列にNULLsが含まれていない場合、2回のフルスキャンが行われます。

単一の集約クエリ

_SELECT 
    MAX(CASE WHEN B IS NULL THEN 1 ELSE 0 END) AS B,
    MAX(CASE WHEN C IS NULL THEN 1 ELSE 0 END) AS C
FROM T
_

これにより、両方の列が同時に処理される可能性があるため、1回のフルスキャンという最悪のケースが発生します。短所は、クエリの非常に早い段階で両方の列でNULLが検出されたとしても、テーブルの残りの部分全体がスキャンされることです。

ユーザー変数

私はこれを行う3番目の方法を考えることができます

_BEGIN TRY
DECLARE @B INT, @C INT, @D INT

SELECT 
    @B = CASE WHEN B IS NULL THEN 1 ELSE @B END,
    @C = CASE WHEN C IS NULL THEN 1 ELSE @C END,
    /*Divide by zero error if both @B and @C are 1.
    Might happen next row as no guarantee of order of
    assignments*/
    @D = 1 / (2 - (@B + @C))
FROM T  
OPTION (MAXDOP 1)       
END TRY
BEGIN CATCH
IF ERROR_NUMBER() = 8134 /*Divide by zero*/
    BEGIN
    SELECT 'B,C both contain NULLs'
    RETURN;
    END
ELSE
    RETURN;
END CATCH

SELECT ISNULL(@B,0),
       ISNULL(@C,0)
_

集約連結クエリの正しい動作は定義されていません。 であり、エラーをスローしてスキャンを終了することは、とにかく非常に恐ろしいソリューションであるため、これは本番コードには適していません。

上記のアプローチの長所を組み合わせた別のオプションはありますか?

編集

これを、これまでに送信された回答の読み取りに関して得た結果で更新するために(@ypercubeのテストデータを使用)

_+----------+------------+------+---------+----------+----------------------+----------+------------------+
|          | 2 * EXISTS | CASE | Kejser  |  Kejser  |        Kejser        | ypercube |       8kb        |
+----------+------------+------+---------+----------+----------------------+----------+------------------+
|          |            |      |         | MAXDOP 1 | HASH GROUP, MAXDOP 1 |          |                  |
| No Nulls |      15208 | 7604 |    8343 | 7604     | 7604                 |    15208 | 8346 (8343+3)    |
| One Null |       7613 | 7604 |    8343 | 7604     | 7604                 |     7620 | 7630 (25+7602+3) |
| Two Null |         23 | 7604 |    8343 | 7604     | 7604                 |       30 | 30 (18+12)       |
+----------+------------+------+---------+----------+----------------------+----------+------------------+
_

@Thomasの答えのために、私は_TOP 3_を_TOP 2_に変更して、早期に終了できるようにしました。私はその回答のデフォルトで並列プランを得たので、他のプランと比較して読み取り数をより比較できるようにするために、_MAXDOP 1_ヒ​​ントを使用して試してみました。以前のテストのように、テーブル全体を読み取らずにクエリの短絡が発生するのを見たので、結果に多少驚いた。

短絡が私のテストデータの計画は以下です

Shortcircuits

Ypercubeのデータの計画は

Not Shortcircuit

したがって、計画にブロッキングソート演算子を追加します。 _HASH GROUP_ヒ​​ントも試しましたが、それでもすべての行が読み取られます

Not Shortcircuit

したがって、他の選択肢はすべての行をブロックして消費するため、この計画が短絡できるようにするhash match (flow distinct)演算子を取得することが重要と思われます。特にこれを強制するヒントはないと思いますが、どうやら "一般的に、オプティマイザは、フローセットを選択します。この場合、入力セット内の個別の値よりも必要な出力行が少ないと判断されます。" =。

@ypercubeのデータは、各列にNULL値(テーブルのカーディナリティ= 30300)を持つ1行のみを持ち、演算子に出入りする推定行は両方とも_1_です。述語をオプティマイザに対してもう少し不透明にすることにより、Flow Distinct演算子で計画を生成しました。

_SELECT TOP 2 *
FROM (SELECT DISTINCT 
        CASE WHEN b IS NULL THEN NULL ELSE 'foo' END AS b
      , CASE WHEN c IS NULL THEN NULL ELSE 'bar' END AS c
  FROM test T 
  WHERE LEFT(b,1) + LEFT(c,1) IS NULL
) AS DT 
_

編集2

最後に発生したTweakの1つは、NULLで最初に検出された行のB列とC列の両方にNULLがある場合、上記のクエリが必要以上の行を処理する可能性があることです。すぐに終了するのではなく、スキャンを続行します。これを回避する1つの方法は、行がスキャンされるときに行のピボットを解除することです。だから私の最後の修正 Thomas Kejserの答え は以下です

_SELECT DISTINCT TOP 2 NullExists
FROM test T 
CROSS APPLY (VALUES(CASE WHEN b IS NULL THEN 'b' END),
                   (CASE WHEN c IS NULL THEN 'c' END)) V(NullExists)
WHERE NullExists IS NOT NULL
_

述語がWHERE (b IS NULL OR c IS NULL) AND NullExists IS NOT NULLである方が良いでしょうが、以前のテストデータに対して、フローディスティンクトを使用したプランを提供しないのに対し、_NullExists IS NOT NULL_は(以下のプラン)を提供します。 。

Unpivoted

26
Martin Smith

どうですか:

SELECT TOP 3 *
FROM (SELECT DISTINCT 
        CASE WHEN B IS NULL THEN NULL ELSE 'foo' END AS B
        , CASE WHEN C IS NULL THEN NULL ELSE 'bar' END AS C
  FROM T 
  WHERE 
    (B IS NULL AND C IS NOT NULL) 
    OR (B IS NOT NULL AND C IS NULL) 
    OR (B IS NULL AND C IS NULL)
) AS DT
20
Thomas Kejser

私が質問を理解しているように、BまたはCのいずれかがnullである行を実際に返すのではなく、いずれかの列の値にnullが存在するかどうかを知りたい場合。その場合は、その理由は次のとおりです。

Select Top 1 'B as nulls' As Col
From T
Where T.B Is Null
Union All
Select Top 1 'C as nulls'
From T
Where T.C Is Null

SQL 2008 R2と100万行のテストリグで、[クライアント統計]タブから次の結果をミリ秒で取得しました。

Kejser                          2907,2875,2829,3576,3103
ypercube                        2454,1738,1743,1765,2305
OP single aggregate solution    (stopped after 120,000 ms) Wouldn't even finish
My solution                     1619,1564,1665,1675,1674

Nolockヒントを追加すると、結果はさらに速くなります。

Select Top 1 'B as nulls' As Col
From T With(Nolock)
Where T.B Is Null
Union All
Select Top 1 'C as nulls'
From T With(Nolock)
Where T.C Is Null

My solution (with nolock)       42,70,94,138,120

参考までに、Red-gateのSQLジェネレーターを使用してデータを生成しました。 100万行のうち、9,886行はnullのB値を持ち、10,019行はnullのC値を持ちました。

この一連のテストでは、列Bのすべての行に値があります。

Kejser                          245200  Scan count 1, logical reads 367259, physical reads 858, read-ahead reads 367278
                                250540  Scan count 1, logical reads 367259, physical reads 860, read-ahead reads 367280

ypercube(1)                     249137  Scan count 2, logical reads 367276, physical reads 850, read-ahead reads 367278
                                248276  Scan count 2, logical reads 367276, physical reads 869, read-ahead reads 368765

My solution                     250348  Scan count 2, logical reads 367276, physical reads 858, read-ahead reads 367278
                                250327  Scan count 2, logical reads 367276, physical reads 854, read-ahead reads 367278

各テスト(両方のセット)の前に、CHECKPOINTDBCC DROPCLEANBUFFERSを実行しました。

これは、テーブルにnullがない場合の結果です。 ypercubeによって提供される2存在するソリューションは、読み取りと実行時間に関して、私のソリューションとほぼ同じです。これは 高度なスキャン を使用するEnterprise/Developerエディションの利点によるものだと私(私たち)は考えています。 Standardエディション以下のみを使用している場合は、Kejserのソリューションが最も高速なソリューションである可能性があります。

Kejser                          248875  Scan count 1, logical reads 367259, physical reads 860, read-ahead reads 367290

ypercube(1)                     243349  Scan count 2, logical reads 367265, physical reads 851, read-ahead reads 367278
                                242729  Scan count 2, logical reads 367265, physical reads 858, read-ahead reads 367276
                                242531  Scan count 2, logical reads 367265, physical reads 855, read-ahead reads 367278

My solution                     243094  Scan count 2, logical reads 367265, physical reads 857, read-ahead reads 367278
                                243444  Scan count 2, logical reads 367265, physical reads 857, read-ahead reads 367278
6
Thomas

バージョンのSQL-Fiddleでテスト済み: 2008 r2 および 2012 30K行。

  • EXISTSクエリは、Nullを早期に検出すると効率が大幅に向上することを示しています。
  • EXISTSクエリを使用すると、パフォーマンスが向上します。2012年のすべてのケースで、説明できません。
  • 2008R2では、Nullがない場合、他の2つのクエリよりも遅くなります。 Nullの検出が早いほど、取得は高速になり、両方の列に早期にnullがある場合、他の2つのクエリよりもはるかに高速になります。
  • Thomas Kejserのクエリは、MartinのCASEクエリと比較して、2012年にはわずかに、ただし常にパフォーマンスが向上し、2008R2ではパフォーマンスが低下するようです。
  • 2012バージョンは、パフォーマンスがはるかに優れているようです。ただし、オプティマイザの改善だけでなく、SQL-Fiddleサーバーの設定にも関係している可能性があります。

クエリとタイミング。行われるタイミング:

  • Nullがまったくない1番目
  • 2番目の列はBで、小さなNULLに1つのidがあります。
  • 3番目に、両方の列に1つのNULLがあり、それぞれに小さなIDがあります。

ここに行きます(計画に問題があります。後でもう一度試します。今のところリンクに従ってください):


2つのEXISTSサブクエリを持つクエリ

SELECT 
      CASE WHEN EXISTS (SELECT * FROM test WHERE b IS NULL)
             THEN 1 ELSE 0 
      END AS B,
      CASE WHEN EXISTS (SELECT * FROM test WHERE c IS NULL)
             THEN 1 ELSE 0 
      END AS C ;

-------------------------------------
Times in ms (2008R2): 1344 - 596 -  1  
Times in ms   (2012):   26 -  14 -  2

Martin Smithの単一集計クエリ

SELECT 
    MAX(CASE WHEN b IS NULL THEN 1 ELSE 0 END) AS B,
    MAX(CASE WHEN c IS NULL THEN 1 ELSE 0 END) AS C
FROM test ;

--------------------------------------
Times in ms (2008R2):  558 - 553 - 516  
Times in ms   (2012):   37 -  35 -  36

Thomas Kejserのクエリ

SELECT TOP 3 *
FROM (SELECT DISTINCT 
        CASE WHEN B IS NULL THEN NULL ELSE 'foo' END AS b
      , CASE WHEN C IS NULL THEN NULL ELSE 'bar' END AS c
  FROM test T 
  WHERE 
    (B IS NULL AND C IS NOT NULL) 
    OR (B IS NOT NULL AND C IS NULL) 
    OR (B IS NULL AND C IS NULL)
) AS DT ;

--------------------------------------
Times in ms (2008R2):  859 - 705 - 668  
Times in ms   (2012):   24 -  19 -  18

私の提案(1)

WITH tmp1 AS
  ( SELECT TOP (1) 
        id, b, c
    FROM test
    WHERE b IS NULL OR c IS NULL
    ORDER BY id 
  ) 

  SELECT 
      tmp1.*, 
      NULL AS id2, NULL AS b2, NULL AS c2
  FROM tmp1
UNION ALL
  SELECT *
  FROM
    ( SELECT TOP (1)
          tmp1.id, tmp1.b, tmp1.c,
          test.id AS id2, test.b AS b2, test.c AS c2 
      FROM test
        CROSS JOIN tmp1
      WHERE test.id >= tmp1.id
        AND ( test.b IS NULL AND tmp1.c IS NULL
           OR tmp1.b IS NULL AND test.c IS NULL
            )
      ORDER BY test.id
    ) AS x ;

--------------------------------------
Times in ms (2008R2): 1089 - 572 -  16   
Times in ms   (2012):   28 -  15 -   1

出力をいくらか磨く必要がありますが、効率はEXISTSクエリに似ています。 nullがないほうがいいと思いましたが、テストではそうではないことがわかりました。


提案(2)

ロジックを単純化しようとしています:

CREATE TABLE tmp
( id INT
, b CHAR(1000)
, c CHAR(1000)
) ;

DELETE  FROM tmp ;

INSERT INTO tmp 
    SELECT TOP (1) 
        id, b, c
    FROM test
    WHERE b IS NULL OR c IS NULL
    ORDER BY id  ; 

INSERT INTO tmp 
    SELECT TOP (1)
        test.id, test.b, test.c 
      FROM test
        JOIN tmp 
          ON test.id >= tmp.id
      WHERE ( test.b IS NULL AND tmp.c IS NULL
           OR tmp.b IS NULL AND test.c IS NULL
            )
      ORDER BY test.id ;

SELECT *
FROM tmp ;

以前の提案よりも2008R2の方がパフォーマンスは良いようですが、2012年には悪くなります(おそらく、2番目のINSERTは、@ 8kbの回答のように、IFを使用して書き換えることができます)。

------------------------------------------
Times in ms (2008R2): 416+6 - 1+127 -  1+1   
Times in ms   (2012):  14+1 - 0+27  -  0+29
4
ypercubeᵀᴹ

IFステートメントは許可されますか?

これにより、テーブルの1回のパスでBまたはCの存在を確認できます。

DECLARE 
  @A INT, 
  @B CHAR(10), 
  @C CHAR(10)

SET @B = 'X'
SET @C = 'X'

SELECT TOP 1 
  @A = A, 
  @B = B, 
  @C = C
FROM T 
WHERE B IS NULL OR C IS NULL 

IF @@ROWCOUNT = 0 
BEGIN 
  SELECT 'No nulls'
  RETURN
END

IF @B IS NULL AND @C IS NULL
BEGIN
  SELECT 'Both null'
  RETURN
END 

IF @B IS NULL 
BEGIN
  SELECT TOP 1 
    @C = C
  FROM T
  WHERE A > @A
  AND C IS NULL

  IF @B IS NULL AND @C IS NULL 
  BEGIN
    SELECT 'Both null'
    RETURN
  END
  ELSE
  BEGIN
    SELECT 'B is null'
    RETURN
  END
END

IF @C IS NULL 
BEGIN
  SELECT TOP 1 
    @B = B
  FROM T 
  WHERE A > @A
  AND B IS NULL

  IF @C IS NULL AND @B IS NULL
  BEGIN
    SELECT 'Both null'
    RETURN
  END
  ELSE
  BEGIN
    SELECT 'C is null'
    RETURN
  END
END      
4
8kb

EXISTSを使用すると、SQL Serverは存在チェックを実行していることを認識します。最初に一致する値が見つかると、TRUEを返し、検索を停止します。

2つの列を連結し、いずれかがnullの場合、結果はnullになります。

例えば

null + 'a' = null

このコードを確認してください

IF EXISTS (SELECT 1 FROM T WHERE B+C is null)
SELECT Top 1 ISNULL(B,'B ') + ISNULL(C,'C') as [Nullcolumn] FROM T WHERE B+C is null
0
AmmarR