複数の列でEXISTSを効率的にチェックする方法は?


26

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

次のテーブル構造を想定

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

及び要件はNULL可能列のいずれかかどうかを決定することであるBまたはC実際に含むNULL値(及び場合どのように一方(S))。

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

これにアプローチする方法はいくつか考えられますが、すべてに弱点があります。

2つの別個のEXISTSステートメント。これには、a NULLが見つかるとすぐにクエリがスキャンを停止できるという利点があります。ただし、実際に両方の列にが含まれていないNULL場合、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、クエリの非常に早い段階で両方の列にが見つかった場合でも、テーブルの残りの部分全体をスキャンすることになります。

ユーザー変数

私がすることができますこれを行うための第三の方法を考えます

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 3TOP 2ました。私はその答えに対してデフォルトで並列プランを取得したのでMAXDOP 1、読み取り回数を他のプランと比較できるようにするためのヒントも試してみました。以前のテストで、テーブル全体を読み取らずにクエリが短絡するのを見ていたため、結果に多少驚きました。

短絡のテストデータの計画は以下のとおりです

短絡

ypercubeのデータの計画は

短絡しない

そのため、プランにブロッキングソート演算子が追加されます。私もHASH GROUPヒントを試しましたが、それでもすべての行を読んでしまいます

短絡しない

そのため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

私に起こった最後の微調整の1つは、上記のクエリは、最初に遭遇した行のNULLBと列の両方にNULLがある場合、必要以上の行を処理する可能性があることCです。すぐに終了するのではなく、スキャンを続行します。これを回避する1つの方法は、スキャンされる行のピボットを解除することです。トーマス・ケイサーの答えに対する私の最後の修正は以下の通りです

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が以前のテストデータに対して、Flow Distinctのプランを提供しないのに対し、NullExists IS NOT NULL1つは提供する(以下のプラン)のが良いでしょう。

ピボットなし

回答:


20

どうですか:

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

私はこのアプローチが好きです。ただし、質問の編集で対処する可能性のある問題がいくつかあります。書かれたようTOP 3ちょうど可能性がありTOP 2、それは以下のそれぞれの1を見つけるまで、それはスキャンする現在のように(NOT_NULL,NULL)(NULL,NOT_NULL)(NULL,NULL)。これら3つのうち2つで十分(NULL,NULL)です。最初に見つかった場合は、2つ目も必要ありません。また、短絡させるために計画を介して、明確なを実装する必要があるだろうhash match (flow distinct)ではなく、オペレータhash match (aggregate)またはdistinct sort
マーティン・スミス

6

私は質問を理解しているので、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 Generatorを使用してデータを生成しました。私の100万行のうち、9,886行にはヌルのB値があり、10,019行にはヌルの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

各テスト(両方のセット)の前に、私はを実行CHECKPOINTしましたDBCC DROPCLEANBUFFERS

表にヌルがない場合の結果を次に示します。ypercubeが提供する2つの既存のソリューションは、読み取りと実行時間の点で私のものとほぼ同じであることに注意してください。これは、Advanced Scanningを使用する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

4

されている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

:バージョンでSQL-フィドルでテスト2008 R2および2012 30K行を持ちます。

  • EXISTSクエリを示し、それが早期にヌルを見つける効率の大きなメリット-期待されています。
  • EXISTSクエリを使用するとパフォーマンスが向上します-2012年のすべてのケースで、説明できません。
  • 2008R2では、Nullがない場合、他の2つのクエリよりも遅くなります。Nullを見つけるのが早いほど速くなり、両方の列に早くNULLがある場合、他の2つのクエリよりもはるかに高速です。
  • Thomas Kejserのクエリは、MartinのCASEクエリと比較して、わずかに実行されますが、2012年には常に良くなり、2008R2では悪くなります。
  • 2012バージョンのパフォーマンスははるかに優れているようです。ただし、オプティマイザーの改善だけでなく、SQL-Fiddleサーバーの設定にも関係する場合があります。

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

  • ヌルなしの1番目
  • 2 Bつ目NULLは小さな列に1つありidます。
  • 3つ目は、両方の列NULLに小さなIDでそれぞれ1つあります。

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


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

マーティンスミスの単一集計クエリ

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年にはさらに悪くなります(おそらく、@ 8kbの答えのように、2番目のコードをINSERT書き換えることができますIF)。

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

0

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

-3

どうですか:

select 
    exists(T.B is null) as 'B is null',
    exists(T.C is null) as 'C is null'
from T;

これが機能する場合(テストしていません)、2列の1行のテーブルが生成され、各列はTRUEまたはFALSEになります。私は効率をテストしませんでした。


2
これが他のDBMSで有効であっても、正しいセマンティクスを持っているとは思えません。仮定するとT.B is null、その後ブール結果として扱われるEXISTS(SELECT true)EXISTS(SELECT false)の両方のリターンは本当でしょう。このMySQLの例では、どちらも実際には行わない時に両方の列はNULLが含まれていることを示している
マーティン・スミス
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.