これは私が定期的に直面する問題であり、まだ良い解決策を見つけていません。
次のテーブル構造を想定します
_CREATE TABLE T
(
A INT PRIMARY KEY,
B CHAR(1000) NULL,
C CHAR(1000) NULL
)
_
また、要件は、null許容列B
またはC
のいずれかに実際にNULL
値が含まれているかどうか(および含まれている場合はどちらか)を判別することです。
また、テーブルに数百万行が含まれていると仮定します(このクラスのクエリのより一般的なソリューションに興味があるので、ピークになる可能性のある列統計はありません)。
これに取り組む方法はいくつか考えられますが、すべてに弱点があります。
2つの個別のEXISTS
ステートメント。これには、NULL
が検出されるとすぐにクエリがスキャンを停止できるという利点があります。ただし、実際に両方の列にNULL
sが含まれていない場合、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
_ヒントを使用して試してみました。以前のテストのように、テーブル全体を読み取らずにクエリの短絡が発生するのを見たので、結果に多少驚いた。
短絡が私のテストデータの計画は以下です
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
最後に発生した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
_は(以下のプラン)を提供します。 。
どうですか:
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
私が質問を理解しているように、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
各テスト(両方のセット)の前に、CHECKPOINT
とDBCC 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
バージョンのSQL-Fiddleでテスト済み: 2008 r2 および 2012 30K行。
EXISTS
クエリは、Nullを早期に検出すると効率が大幅に向上することを示しています。EXISTS
クエリを使用すると、パフォーマンスが向上します。2012年のすべてのケースで、説明できません。CASE
クエリと比較して、2012年にはわずかに、ただし常にパフォーマンスが向上し、2008R2ではパフォーマンスが低下するようです。クエリとタイミング。行われるタイミング:
B
で、小さなNULL
に1つのid
があります。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
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
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