sql >> データベース >  >> RDS >> Database

NULLの複雑さ–パート2

    この記事は、NULLの複雑さに関するシリーズの第2回です。先月、あらゆる種類の欠落値のSQLマーカーとしてNULLを導入しました。 SQLには、欠落しているものと該当するものを区別する機能がないことを説明しました。 (A値)および欠落していて適用できない (I値)マーカー。また、NULLを含む比較が定数、変数、パラメーター、および列でどのように機能するかについても説明しました。今月は、さまざまなT-SQL要素でのNULL処理の不整合について説明し、議論を続けます。

    いくつかの例では、先月のようにサンプルデータベースTSQLV5を引き続き使用します。このデータベースを作成してデータを取り込むスクリプトはここにあり、そのER図はここにあります。

    NULL処理の不整合

    すでに集まっているように、NULLの扱いは簡単ではありません。混乱と複雑さの一部は、NULLの処理が、同様の操作のT-SQLの異なる要素間で一貫していない可能性があるという事実と関係があります。次のセクションでは、線形計算と集計計算、ON / WHERE / HAVING句、CHECK制約とCHECKオプション、IF / WHILE / CASE要素、MERGEステートメント、区別とグループ化、および順序と一意性でのNULL処理について説明します。

    線形計算と集計計算

    T-SQLは、標準SQLにも当てはまりますが、行全体にSUM、MIN、MAXなどの実際の集計関数を適用する場合と、列全体に線形関数と同じ計算を適用する場合とでは、異なるNULL処理ロジックを使用します。この違いを示すために、次のコードを実行して作成および入力する#T1および#T2という2つのサンプルテーブルを使用します。

    DROP TABLE IF EXISTS #T1, #T2;
     
    SELECT * INTO #T1 FROM ( VALUES(10, 5, NULL) ) AS D(col1, col2, col3);
     
    SELECT * INTO #T2 FROM ( VALUES(10),(5),(NULL) ) AS D(col1);

    テーブル#T1には、col1、col2、およびcol3という3つの列があります。現在、列値がそれぞれ10、5、NULLの1つの行があります。

    SELECT * FROM #T1;
    col1        col2        col3
    ----------- ----------- -----------
    10          5           NULL

    テーブル#T2には、col1という1つの列があります。現在、col1に値10、5、NULLの3つの行があります。

    SELECT * FROM #T2;
    col1
    -----------
    10
    5
    NULL

    列全体の線形加算などの最終的な集計計算を適用する場合、NULL入力が存在するとNULL結果が生成されます。次のクエリは、この動作を示しています。

    SELECT col1 + col2 + col3 AS total
    FROM #T1;

    このクエリは次の出力を生成します:

    total
    -----------
    NULL

    逆に、行全体に適用される実際の集計関数は、NULL入力を無視するように設計されています。次のクエリは、SUM関数を使用してこの動作を示しています。

    SELECT SUM(col1) AS total
    FROM #T2;

    このクエリは次の出力を生成します:

    total
    -----------
    15
    
    Warning: Null value is eliminated by an aggregate or other SET operation.

    無視されたNULL入力の存在を示すSQL標準によって義務付けられた警告に注意してください。 ANSI_WARNINGSセッションオプションをオフにすることで、このような警告を抑制することができます。

    同様に、入力式に適用すると、COUNT関数はNULL以外の入力値を持つ行の数をカウントします(単に行の数をカウントするCOUNT(*)とは対照的です)。たとえば、上記のクエリでSUM(col1)をCOUNT(col1)に置き換えると、2のカウントが返されます。

    不思議なことに、NULLを許可しないものとして定義されている列にCOUNT集計を適用すると、オプティマイザーは式COUNT()をCOUNT(*)に変換します。これにより、問題の列を含むインデックスの使用を要求するのではなく、カウントの目的で任意のインデックスを使用できるようになります。これは、データの一貫性と整合性を確保する以外のもう1つの理由であり、NOTNULLなどの制約を適用するように促す必要があります。このような制約により、オプティマイザはより最適な代替案を検討し、不要な作業を回避する際の柔軟性を高めることができます。

    このロジックに基づいて、AVG関数は非NULL値の合計を非NULL値の数で除算します。例として次のクエリを考えてみましょう。

    SELECT AVG(1.0 * col1) AS avgall
    FROM #T2;

    ここで、非NULL col1値の合計15は、非NULL値2のカウントで除算されます。col1に数値リテラル1.0を掛けて、整数入力値を数値値に暗黙的に変換し、整数ではなく数値除算を取得します。分割。このクエリは次の出力を生成します:

    avgall
    ---------
    7.500000

    同様に、MINおよびMAXアグリゲートはNULL入力を無視します。次のクエリについて考えてみます。

    SELECT MIN(col1) AS mincol1, MAX(col1) AS maxcol1
    FROM #T2;

    このクエリは次の出力を生成します:

    mincol1     maxcol1
    ----------- -----------
    5           10

    線形計算を適用しようとしますが、集計関数のセマンティクスをエミュレートする(NULLを無視する)のは見栄えがよくありません。 SUM、COUNT、およびAVGのエミュレートはそれほど複雑ではありませんが、次のように、すべての入力でNULLをチェックする必要があります。

    SELECT col1, col2, col3,
      CASE
        WHEN COALESCE(col1, col2, col3) IS NULL THEN NULL
        ELSE COALESCE(col1, 0) + COALESCE(col2, 0) + COALESCE(col3, 0)
      END AS sumall,
      CASE WHEN col1 IS NOT NULL THEN 1 ELSE 0 END
        + CASE WHEN col2 IS NOT NULL THEN 1 ELSE 0 END
        + CASE WHEN col3 IS NOT NULL THEN 1 ELSE 0 END AS cntall,
      CASE
        WHEN COALESCE(col1, col2, col3) IS NULL THEN NULL
        ELSE 1.0 * (COALESCE(col1, 0) + COALESCE(col2, 0) + COALESCE(col3, 0))
               / (CASE WHEN col1 IS NOT NULL THEN 1 ELSE 0 END
                    + CASE WHEN col2 IS NOT NULL THEN 1 ELSE 0 END
                    + CASE WHEN col3 IS NOT NULL THEN 1 ELSE 0 END)
      END AS avgall
    FROM #T1;

    このクエリは次の出力を生成します:

    col1        col2        col3        sumall      cntall      avgall
    ----------- ----------- ----------- ----------- ----------- ---------------
    10          5           NULL        15          2           7.500000000000

    線形計算として最小値または最大値を3つ以上の入力列に適用しようとすると、NULLを無視するロジックを追加する前でも、直接または間接的に複数のCASE式をネストする必要があるため(列エイリアスを再利用する場合)、非常に注意が必要です。たとえば、NULLを無視する部分を除いて、#T1のcol1、col2、col3の最大値を計算するクエリを次に示します。

    SELECT col1, col2, col3, 
      CASE WHEN col1 IS NULL OR col2 IS NULL OR col3 IS NULL THEN NULL ELSE max2 END AS maxall
    FROM #T1
      CROSS APPLY (VALUES(CASE WHEN col1 >= col2 THEN col1 ELSE col2 END)) AS A1(max1)
      CROSS APPLY (VALUES(CASE WHEN max1 >= col3 THEN max1 ELSE col3 END)) AS A2(max2);

    このクエリは次の出力を生成します:

    col1        col2        col3        maxall
    ----------- ----------- ----------- -----------
    10          5           NULL        NULL

    クエリプランを調べると、最終結果を計算する次の拡張式が見つかります。

    [Expr1005] = Scalar Operator(CASE WHEN CASE WHEN [#T1].[col1] IS NOT NULL THEN [#T1].[col1] ELSE 
      CASE WHEN [#T1].[col2] IS NOT NULL THEN [#T1].[col2] 
        ELSE [#T1].[col3] END END IS NULL THEN NULL ELSE 
      CASE WHEN CASE WHEN [#T1].[col1]>=[#T1].[col2] THEN [#T1].[col1] 
        ELSE [#T1].[col2] END>=[#T1].[col3] THEN 
      CASE WHEN [#T1].[col1]>=[#T1].[col2] THEN [#T1].[col1] 
        ELSE [#T1].[col2] END ELSE [#T1].[col3] END END)

    そして、それは3つの列だけが関係しているときです。十数本の列が関係していると想像してみてください!

    次に、これにNULLを無視するロジックを追加します。

    SELECT col1, col2, col3, max2 AS maxall
    FROM #T1
      CROSS APPLY (VALUES(CASE WHEN col1 >= col2 OR col2 IS NULL THEN col1 ELSE col2 END)) AS A1(max1)
      CROSS APPLY (VALUES(CASE WHEN max1 >= col3 OR col3 IS NULL THEN max1 ELSE col3 END)) AS A2(max2);

    このクエリは次の出力を生成します:

    col1        col2        col3        maxall
    ----------- ----------- ----------- -----------
    10          5           NULL        10

    Oracleには、入力値に線形計算としてそれぞれ最小計算と最大計算を適用するGREATESTとLEASTと呼ばれる関数のペアがあります。これらの関数は、ほとんどの線形計算と同様に、NULL入力が与えられるとNULLを返します。 T-SQLで同様の機能を取得するように求める未解決のフィードバック項目がありましたが、この要求は最新のフィードバックサイトの変更に移植されませんでした。 Microsoftがそのような関数をT-SQLに追加する場合、NULLを無視するかどうかを制御するオプションがあると便利です。

    一方、NULLを無視する実際の集計関数のセマンティクスを使用して、あらゆる種類の集計を列全体で線形として計算する前述の手法と比較して、はるかに洗練された手法があります。 CROSS APPLY演算子と、列を行にローテーションして集計を実際の集計関数として適用するテーブル値コンストラクターに対して派生テーブルクエリを組み合わせて使用​​します。 MINとMAXの計算を示す例を次に示しますが、この手法は任意の集計関数で使用できます。

    SELECT col1, col2, col3, maxall, minall
    FROM #T1 CROSS APPLY
      (SELECT MAX(mycol), MIN(mycol)
       FROM (VALUES(col1),(col2),(col3)) AS D1(mycol)) AS D2(maxall, minall);

    このクエリは次の出力を生成します:

    col1        col2        col3        maxall      minall
    ----------- ----------- ----------- ----------- -----------
    10          5           NULL        10          5

    反対の場合はどうなりますか?行全体の集計を計算する必要があるが、NULL入力がある場合はNULLを生成する場合はどうなりますか?たとえば、#T1のすべてのcol1値を合計する必要があるが、入力のいずれかがNULLの場合はNULLを返すとします。これは、次の手法で実現できます。

    SELECT SUM(col1) * NULLIF(MIN(CASE WHEN col1 IS NULL THEN 0 ELSE 1 END), 0) AS sumall
    FROM #T2;

    NULL入力の場合は0を返し、NULL以外の入力の場合は1を返すCASE式にMIN集計を適用します。 NULL入力がある場合、MIN関数の結果は0です。それ以外の場合は1です。次に、NULLIF関数を使用して、0の結果をNULLに変換します。次に、NULLIF関数の結果に元の合計を掛けます。 NULL入力がある場合は、元の合計にNULLを掛けて、NULLを生成します。 NULL入力がない場合は、元の合計の結果に1を掛けて、元の合計を算出します。

    NULL入力に対してNULLを生成する線形計算に戻ると、次のクエリが示すように、同じロジックが+演算子を使用した文字列連結に適用されます。

    USE TSQLV5;
     
    SELECT empid, country, region, city,
      country + N',' + region + N',' + city AS emplocation
    FROM HR.Employees;

    このクエリは次の出力を生成します:

    empid       country         region          city            emplocation
    ----------- --------------- --------------- --------------- ----------------
    1           USA             WA              Seattle         USA,WA,Seattle
    2           USA             WA              Tacoma          USA,WA,Tacoma
    3           USA             WA              Kirkland        USA,WA,Kirkland
    4           USA             WA              Redmond         USA,WA,Redmond
    5           UK              NULL            London          NULL
    6           UK              NULL            London          NULL
    7           UK              NULL            London          NULL
    8           USA             WA              Seattle         USA,WA,Seattle
    9           UK              NULL            London          NULL

    区切り文字としてコンマを使用して、従業員の場所の部分を1つの文字列に連結する必要があります。ただし、NULL入力は無視する必要があります。代わりに、入力のいずれかがNULLの場合、結果としてNULLを取得します。 CONCAT_NULL_YIELDS_NULLセッションオプションをオフにするものもあります。これにより、連結の目的でNULL入力が空の文字列に変換されますが、このオプションは非標準の動作を適用するため、お勧めしません。さらに、NULL入力がある場合、複数の連続した区切り文字が残ります。これは通常、望ましい動作ではありません。もう1つのオプションは、ISNULLまたはCOALESCE関数を使用してNULL入力を空の文字列に明示的に置き換えることですが、これは通常、長い冗長コードになります。より洗練されたオプションは、SQL Server 2017で導入されたCONCAT_WS関数を使用することです。この関数は、最初の入力として提供されたセパレーターを使用して、NULLを無視して入力を連結します。この関数を使用したソリューションクエリは次のとおりです。

    SELECT empid, country, region, city,
      CONCAT_WS(N',', country, region, city) AS emplocation
    FROM HR.Employees;

    このクエリは次の出力を生成します:

    empid       country         region          city            emplocation
    ----------- --------------- --------------- --------------- ----------------
    1           USA             WA              Seattle         USA,WA,Seattle
    2           USA             WA              Tacoma          USA,WA,Tacoma
    3           USA             WA              Kirkland        USA,WA,Kirkland
    4           USA             WA              Redmond         USA,WA,Redmond
    5           UK              NULL            London          UK,London
    6           UK              NULL            London          UK,London
    7           UK              NULL            London          UK,London
    8           USA             WA              Seattle         USA,WA,Seattle
    9           UK              NULL            London          UK,London

    ON / WHERE / HAVING

    フィルタリング/マッチングの目的でWHERE、HAVING、およびONクエリ句を使用する場合、これらは3値述語論理を使用することを覚えておくことが重要です。 3値論理が関係している場合、句がTRUE、FALSE、およびUNKNOWNのケースをどのように処理するかを正確に識別したいとします。これらの3つの句は、TRUEの場合を受け入れ、FALSEおよびUNKNOWNの場合を拒否するように設計されています。

    この動作を示すために、次のコードを実行して作成および入力する連絡先というテーブルを使用します。

    DROP TABLE IF EXISTS dbo.Contacts;
    GO
     
    CREATE TABLE dbo.Contacts
    (
      id INT NOT NULL 
        CONSTRAINT PK_Contacts PRIMARY KEY,
      name VARCHAR(10) NOT NULL,
      hourlyrate NUMERIC(12, 2) NULL
        CONSTRAINT CHK_Contacts_hourlyrate CHECK(hourlyrate > 0.00)
    );
     
    INSERT INTO dbo.Contacts(id, name, hourlyrate) VALUES
      (1, 'A', 100.00),(2, 'B', 200.00),(3, 'C', NULL);

    連絡先1と2には適用可能な時間料金があり、連絡先3にはないため、時間料金はNULLに設定されていることに注意してください。正の時給の連絡先を探す次のクエリについて考えてみます。

    SELECT id, name, hourlyrate
    FROM dbo.Contacts
    WHERE hourlyrate > 0.00;

    この述語は、連絡先1と2についてはTRUEと評価され、連絡先3についてはUNKNOWNと評価されるため、出力には連絡先1と2のみが含まれます。

    id          name       hourlyrate
    ----------- ---------- -----------
    1           A          100.00
    2           B          200.00

    ここでの考え方は、述語が真であると確信している場合は、行を返したい、そうでない場合は破棄したいということです。述語も使用する一部の言語要素が異なる動作をすることに気付くまで、これは最初は些細なことのように思えるかもしれません。

    チェック制約とチェックオプション

    CHECK制約は、述部に基づいてテーブルの整合性を強制するために使用するツールです。述部は、テーブルの行を挿入または更新しようとしたときに評価されます。 TRUEの場合を受け入れ、FALSEおよびUNKNOWNの場合を拒否するクエリのフィルタリングおよびマッチング句とは異なり、CHECK制約は、TRUEおよびUNKNOWNの場合を受け入れ、FALSEの場合を拒否するように設計されています。ここでの考え方は、述語が偽であると確信している場合は、試行された変更を拒否したい、そうでない場合は許可したいということです。

    連絡先テーブルの定義を調べると、次のCHECK制約があり、正でない時給の連絡先が拒否されていることがわかります。

    CONSTRAINT CHK_Contacts_hourlyrate CHECK(hourlyrate > 0.00)

    制約は、前のクエリフィルターで使用したものと同じ述語を使用していることに注意してください。

    正の時給で連絡先を追加してみてください:

    INSERT INTO dbo.Contacts(id, name, hourlyrate) VALUES (4, 'D', 150.00);

    この試みは成功します。

    時給がNULLの連絡先を追加してみてください:

    INSERT INTO dbo.Contacts(id, name, hourlyrate) VALUES (5, 'E', NULL);

    CHECK制約はTRUEおよびUNKNOWNの場合を受け入れるように設計されているため、この試行も成功します。これは、クエリフィルターとCHECK制約が異なる動作をするように設計されている場合です。

    正でない時給の連絡先を追加してみてください:

    INSERT INTO dbo.Contacts(id, name, hourlyrate) VALUES (6, 'F', -100.00);

    この試行は次のエラーで失敗します:

    メッセージ547、レベル16、状態0、行454
    INSERTステートメントがCHECK制約「CHK_Contacts_hourlyrate」と競合していました。データベース「TSQLV5」、テーブル「dbo.Contacts」、列「hourlyrate」で競合が発生しました。

    T-SQLでは、CHECKオプションを使用してビューを介して変更の整合性を強制することもできます。ビューを介して変更を適用する限り、このオプションはCHECK制約と同様の目的を果たすと考える人もいます。たとえば、次のビューについて考えてみます。このビューは、述語の時給> 0.00に基づくフィルターを使用し、CHECKオプションで定義されています。

    CREATE OR ALTER VIEW dbo.MyContacts
    AS
    SELECT id, name, hourlyrate
    FROM dbo.Contacts
    WHERE hourlyrate > 0.00
    WITH CHECK OPTION;

    結局のところ、CHECK制約とは異なり、ビューのCHECKオプションは、TRUEの場合を受け入れ、FALSEとUNKNOWNの両方の場合を拒否するように設計されています。そのため、実際には、整合性を強化する目的でも、クエリフィルタが通常行うように動作するように設計されています。

    ビューから正の時給の行を挿入してみてください:

    INSERT INTO dbo.MyContacts(id, name, hourlyrate) VALUES (7, 'G', 300.00);

    この試みは成功します。

    ビューから時給がNULLの行を挿入してみてください:

    INSERT INTO dbo.MyContacts(id, name, hourlyrate) VALUES (8, 'H', NULL);

    この試行は次のエラーで失敗します:

    メッセージ550、レベル16、状態1、行473
    ターゲットビューがWITHCHECK OPTIONを指定しているか、WITH CHECK OPTIONを指定するビューにまたがっており、操作の結果の1つ以上の行がそうではなかったため、挿入または更新の試行が失敗しましたCHECKOPTION制約の下で適格です。

    ここでの考え方は、ビューにCHECKオプションを追加すると、ビューによって返される行をもたらす変更のみを許可する必要があるということです。これは、CHECK制約を使用した考え方とは少し異なります。つまり、述語が偽であると確信できる変更を拒否します。これは少し混乱する可能性があります。時給をNULLに設定する変更をビューで許可する場合は、OR時給がNULLを追加することにより、それらも許可するクエリフィルターが必要です。 CHECK制約とCHECKオプションは、UNKNOWNの場合とは異なる動作をするように設計されていることを理解する必要があります。前者はそれを受け入れますが、後者はそれを拒否します。

    上記のすべての変更後、連絡先テーブルをクエリします。

    SELECT id, name, hourlyrate
    FROM dbo.Contacts;

    この時点で、次の出力が得られるはずです。

    id          name       hourlyrate
    ----------- ---------- -----------
    1           A          100.00
    2           B          200.00
    3           C          NULL
    4           D          150.00
    5           E          NULL
    7           G          300.00

    IF / WHILE / CASE

    IF、WHILE、およびCASE言語要素は、述語で機能します。

    IFステートメントは次のように設計されています。

    IF <predicate>
      <statement or BEGIN-END block when TRUE>
    ELSE
      <statement or BEGIN-END block when FALSE or UNKNOWN>

    IF句の後にTRUEブロックがあり、ELSE句の後にFALSEブロックがあると予想するのは直感的ですが、述語がFALSEまたはUNKNOWNの場合、ELSE句が実際にアクティブになることを理解する必要があります。理論的には、3値論理言語には、3つのケースを分離したIFステートメントが含まれている可能性があります。このようなもの:

    IF <predicate>
      WHEN TRUE
        <statement or BEGIN-END block when TRUE>
      WHEN FALSE
        <statement or BEGIN-END block when FALSE>
      WHEN UNKNOWN
        <statement or BEGIN-END block when UNKNOWN>

    また、論理的な結果の組み合わせを許可して、FALSEとUNKNOWNを1つのセクションに組み合わせたい場合は、次のようなものを使用できます。

    IF <predicate>
      WHEN TRUE
        <statement or BEGIN-END block when TRUE>
      WHEN FALSE OR UNKNOWN
        <statement or BEGIN-END block when FALSE OR UNKNOWN>

    一方、IF-ELSEステートメントをネストし、IS NULL演算子を使用してオペランド内のNULLを明示的に検索することにより、このような構造をエミュレートできます。

    WHILEステートメントにはTRUEブロックしかありません。次のように設計されています:

    WHILE <predicate>
      <statement or BEGIN-END block when TRUE>

    述語がTUREである間、ループの本体を形成するステートメントまたはBEGIN-ENDブロックがアクティブ化されます。述語がFALSEまたはUNKNOWNになるとすぐに、制御はWHILEループに続くステートメントに渡されます。

    コードを実行するステートメントであるIFやWHILEとは異なり、CASEは値を返す式です。 検索されたの構文 CASEの式は次のとおりです。

    CASE
      WHEN <predicate 1> THEN <expression 1 when TRUE>
      WHEN <predicate 2> THEN <expression 2 when TRUE >
      ...
      WHEN <predicate n> THEN <expression n when TRUE >
      ELSE <else expression when all are FALSE or UNKNOWN>
    END

    CASE式は、TRUEと評価される最初のWHEN述部に対応するTHEN節に続く式を返すように設計されています。 ELSE句がある場合、WHEN述部がTRUEでない場合(すべてFALSEまたはUNKNOWN)にアクティブになります。明示的なELSE句がない場合、暗黙的なELSENULLが使用されます。 UNKNOWNケースを個別に処理する場合は、IS NULL演算子を使用して、述語のオペランドでNULLを明示的に検索できます。

    シンプル CASE式は、ソース式と比較された式の間で暗黙の等式ベースの比較を使用します。

    CASE <source expression>
      WHEN <comp expression 1> THEN <result expression 1 when TRUE>
      WHEN <comp expression 2> THEN <result expression 2 when TRUE >
      ...
      WHEN <comp expression n> THEN <result expression n when TRUE >
      ELSE <else result expression when all are FALSE or UNKNOWN>
    END

    単純なCASE式は、3値論理の処理に関して、検索されたCASE式と同様に設計されていますが、比較では暗黙の等式ベースの比較が使用されるため、UNKNOWNケースを個別に処理することはできません。 WHEN句で比較された式の1つでNULLを使用しようとしても、ソース式がNULLの場合でも比較の結果がTRUEにならないため、意味がありません。次の例を考えてみましょう:

    DECLARE @input AS INT = NULL;
     
    SELECT CASE @input WHEN NULL THEN 'Input is NULL' ELSE 'Input is not NULL' END;

    これは暗黙的に次のように変換されます:

    DECLARE @input AS INT = NULL;
     
    SELECT CASE WHEN @input = NULL THEN 'Input is NULL' ELSE 'Input is not NULL' END;

    したがって、結果は次のようになります。

    入力がNULLではありません

    NULL入力を検出するには、次のように、検索されたCASE式の構文とISNULL演算子を使用する必要があります。

    DECLARE @input AS INT = NULL;
     
    SELECT CASE WHEN @input IS NULL THEN 'Input is NULL' ELSE 'Input is not NULL' END;

    今回の結果は次のとおりです。

    入力がNULLです

    マージ

    MERGEステートメントは、ソースからターゲットにデータをマージするために使用されます。マージ述語を使用して、次のケースを識別し、ターゲットに対してアクションを適用します。

    • ソース行はターゲット行と一致します(マージ述語がTRUEであるソース行に一致が見つかった場合にアクティブ化されます):ターゲットに対してUPDATEまたはDELETEを適用します
    • ソース行がターゲット行と一致しません(マージ述語がTRUEであり、すべての述語がFALSEまたはUNKNOWNであるソース行に一致が見つからない場合にアクティブ化されます):ターゲットに対してINSERTを適用します
    • >
    • ターゲット行がソース行と一致しません(マージ述語がTRUEであり、すべての述語がFALSEまたはUNKNOWNであるターゲット行に一致するものが見つからない場合にアクティブ化されます):ターゲットに対してUPDATEまたはDELETEを適用します

    3つのシナリオはすべて、TRUEを1つのグループに、FALSEまたはUNKNOWNを別のグループに分けます。 TRUE、FALSEの処理、およびUNKNOWNのケースの処理について個別のセクションを取得することはできません。

    これを示すために、次のコードを実行して作成および入力するT3というテーブルを使用します。

    DROP TABLE IF EXISTS dbo.T3;
    GO
     
    CREATE TABLE dbo.T3(col1 INT NULL, col2 INT NULL, CONSTRAINT UNQ_T3 UNIQUE(col1));
     
    INSERT INTO dbo.T3(col1) VALUES(1),(2),(NULL);

    次のMERGEステートメントを検討してください。

    MERGE INTO dbo.T3 AS TGT
    USING (VALUES(1, 100), (3, 300)) AS SRC(col1, col2)
      ON SRC.col1 = TGT.col1
    WHEN MATCHED THEN UPDATE
      SET TGT.col2 = SRC.col2
    WHEN NOT MATCHED THEN INSERT(col1, col2) VALUES(SRC.col1, SRC.col2)
    WHEN NOT MATCHED BY SOURCE THEN UPDATE
      SET col2 = -1;
     
    SELECT col1, col2 FROM dbo.T3;

    col1が1のソース行は、col1が1(述部がTRUE)のターゲット行と一致するため、ターゲット行のcol2は100に設定されます。

    col1が3であるソース行は、どのターゲット行とも一致しません(すべての述語がFALSEまたはUNKNOWNであるため)。したがって、col1値として3、col2値として300の新しい行がT3に挿入されます。

    col1が2で、col1がNULLであるターゲット行は、どのソース行とも一致しません(すべての行で、述語はFALSEまたはUNKNOWNです)。したがって、どちらの場合も、ターゲット行のcol2は-1に設定されます。

    T3に対するクエリは、上記のMERGEステートメントを実行した後、次の出力を返します。

    col1        col2
    ----------- -----------
    1           100
    2           -1
    NULL        -1
    3           300

    テーブルT3を維持します。後で使用します。

    識別性とグループ化

    等式および不等式演算子を使用して行われる比較とは異なり、区別およびグループ化の目的で行われる比較では、NULLがグループ化されます。 1つのNULLは別のNULLと区別されないと見なされますが、NULLは非NULL値と区別されると見なされます。したがって、DISTINCT句を適用すると、重複して発生するNULLが削除されます。次のクエリはこれを示しています:

    SELECT DISTINCT country, region FROM HR.Employees;

    このクエリは次の出力を生成します:

    country         region
    --------------- ---------------
    UK              NULL
    USA             WA

    米国の国と地域がNULLの複数の従業員がいて、重複を削除した後、結果には組み合わせが1回だけ表示されます。

    区別のように、次のクエリが示すように、グループ化もNULLをグループ化します。

    SELECT country, region, COUNT(*) AS numemps
    FROM HR.Employees
    GROUP BY country, region;

    このクエリは次の出力を生成します:

    country         region          numemps
    --------------- --------------- -----------
    UK              NULL            4
    USA             WA              5

    ここでも、英国の国と地域がNULLの4人の従業員全員がグループ化されました。

    注文

    順序付けでは、複数のNULLが同じ順序付け値を持つものとして扱われます。 SQL標準では、NULL以外の値と比較して、NULLを最初に順序付けるか最後に順序付けるかを選択するのは実装に任されています。 Microsoftは、SQL Serverの非NULLと比較してNULLの順序値が低いと見なすことを選択したため、昇順の方向を使用する場合、T-SQLは最初にNULLを順序付けます。次のクエリはこれを示しています:

    SELECT id, name, hourlyrate
    FROM dbo.Contacts
    ORDER BY hourlyrate;

    このクエリは次の出力を生成します:

    id          name       hourlyrate
    ----------- ---------- -----------
    3           C          NULL
    5           E          NULL
    1           A          100.00
    4           D          150.00
    2           B          200.00
    7           G          300.00

    来月は、このトピックについてさらに詳しく説明し、NULLの順序付けの動作を制御できる標準要素と、T-SQLでのこれらの要素の回避策について説明します。

    独自性

    UNIQUE制約または一意性インデックスのいずれかを使用してNULL可能列に一意性を適用する場合、T-SQLはNULLを非NULL値と同じように扱います。あるNULLが別のNULLから一意でないかのように、重複するNULLを拒否します。

    テーブルT3のcol1にUNIQUE制約が定義されていることを思い出してください。その定義は次のとおりです。

    CONSTRAINT UNQ_T3 UNIQUE(col1)

    T3にクエリを実行して、現在の内容を確認します。

    SELECT * FROM dbo.T3;

    この記事の前の例のT3に対してすべての変更を実行した場合、次の出力が得られるはずです。

    col1        col2
    ----------- -----------
    1           100
    2           -1
    NULL        -1
    3           300

    col1にNULLを含む2番目の行を追加しようとしました:

    INSERT INTO dbo.T3(col1, col2) VALUES(NULL, 400);

    次のエラーが発生します:

    メッセージ2627、レベル14、状態1、行558
    UNIQUEKEY制約「UNQ_T3」の違反。オブジェクト'dbo.T3'に重複するキーを挿入できません。重複するキー値は()です。

    この動作は実際には非標準です。来月は、標準仕様と、それをT-SQLでエミュレートする方法について説明します。

    結論

    NULLの複雑さに関するシリーズのこの第2部では、さまざまなT-SQL要素間のNULL処理の不整合に焦点を当てました。線形計算と集計計算、フィルタリングとマッチング句、CHECK制約とCHECKオプション、IF、WHILE、CASE要素、MERGEステートメント、区別とグループ化、順序付け、一意性について説明しました。私が取り上げた矛盾は、使用しているプラ​​ットフォームでのNULLの処理を正しく理解し、正しく堅牢なコードを記述できるようにすることがいかに重要であるかをさらに強調しています。来月は、T-SQLでは利用できないSQL標準のNULL処理オプションについて説明し、T-SQLでサポートされている回避策を提供してシリーズを続けます。


    1. herokuでdatabase_urlを変更するにはどうすればよいですか?

    2. 致命的なエラー:nullでメンバー関数query()を呼び出す

    3. PL /SQLで日付形式をMM/DD / YYYYからYYYY-MM-DDに変更するにはどうすればよいですか?

    4. SQLクエリを高速化する方法