これは私が定期的に出くわす問題であり、まだ良い解決策を見つけていません。
次のテーブル構造を想定
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 3
しTOP 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つは、上記のクエリは、最初に遭遇した行のNULL
列B
と列の両方に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 NULL
1つは提供する(以下のプラン)のが良いでしょう。
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