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

CASE式の汚い秘密

    CASE 式は、T-SQLで私のお気に入りの構成要素の1つです。これは非常に柔軟性があり、SQLServerが述語を評価する順序を制御する唯一の方法である場合があります。
    ただし、誤解されることがよくあります。

    T-SQL CASE式とは何ですか?

    T-SQLでは、CASE 1つ以上の可能な式を評価し、最初の適切な式を返す式です。式という用語はここでは少しオーバーロードされている可能性がありますが、基本的には、変数、列、文字列リテラル、さらには組み込み関数やスカラー関数の出力など、単一のスカラー値として評価できるものです。 。

    T-SQLには2つの形式のCASEがあります。

    • 単純なCASE式 –同等性のみを評価する必要がある場合:

      CASE <input> WHEN <eval> THEN <return> … [ELSE <return>] END

    • 検索されたCASE式 –不等式、LIKE、IS NOT NULLなどのより複雑な式を評価する必要がある場合:

      CASE WHEN <input_bool> THEN <return> … [ELSE <return>] END

    戻り式は常に単一の値であり、出力データ型はデータ型の優先順位によって決定されます。

    私が言ったように、CASE表現はしばしば誤解されます。ここにいくつかの例があります:

    CASEは式であり、ステートメントではありません

    ほとんどの人にとって重要ではない可能性があり、おそらくこれは私のペダンティックな側面ですが、多くの人がそれをCASEと呼んでいます ステートメント –ドキュメントでステートメントを使用しているMicrosoftを含む および 時々交換可能。これはやや煩わしいと思います(行/記録など) および列/フィールド )そして、それはほとんどセマンティクスですが、式とステートメントの間には重要な違いがあります。式は結果を返します。人々がCASEについて考えるとき ステートメントとして 、次のようなコード短縮の実験につながります:

    SELECT CASE [status]
        WHEN 'A' THEN
            StatusLabel      = 'Authorized',
            LastEvent        = AuthorizedTime
        WHEN 'C' THEN
            StatusLabel      = 'Completed',
            LastEvent        = CompletedTime
        END
    FROM dbo.some_table;

    またはこれ:

    SELECT CASE WHEN @foo = 1 THEN
        (SELECT foo, bar FROM dbo.fizzbuzz)
    ELSE
        (SELECT blat, mort FROM dbo.splunge)
    END;

    このタイプのフロー制御ロジックは、CASEで可能になる場合があります ステートメント 他の言語(VBScriptなど)ではありますが、Transact-SQLのCASEではありません CASEを使用するには 同じクエリロジック内で、CASEを使用する必要があります 出力列ごと:

    SELECT 
      StatusLabel = CASE [status]
          WHEN 'A' THEN 'Authorized' 
          WHEN 'C' THEN 'Completed' END,
      LastEvent = CASE [status]
          WHEN 'A' THEN AuthorizedTime
          WHEN 'C' THEN CompletedTime END
    FROM dbo.some_table;

    CASEは常に短絡するとは限りません

    公式ドキュメントでは、式全体が短絡することを示唆していました。つまり、式は左から右に評価され、一致すると評価が停止します。

    CASEステートメント[sic!]は、その条件を順番に評価し、条件が満たされた最初の条件で停止します。

    ただし、これは常に正しいとは限りません。そして、その名誉のために、より最新のバージョンでは、ページはこれが保証されていない1つのシナリオを説明しようとしました。しかし、それは物語の一部にすぎません:

    状況によっては、CASEステートメント[sic!]が式の結果を入力として受け取る前に、式が評価されます。これらの式の評価でエラーが発生する可能性があります。 CASEステートメント[sic!]のWHEN引数に表示される集計式が最初に評価され、次にCASEステートメント[sic!]に提供されます。たとえば、次のクエリでは、MAX集計の値を生成するときに、ゼロ除算エラーが生成されます。これは、CASE式を評価する前に発生します。

    ゼロ除算の例は非常に簡単に再現できます。dba.stackexchange.comのこの回答で示しました:

    DECLARE @i INT = 1;
    SELECT CASE WHEN @i = 1 THEN 1 ELSE MIN(1/0) END;

    結果:

    メッセージ8134、レベル16、状態1
    ゼロ除算エラーが発生しました。

    簡単な回避策があります(ELSE (SELECT MIN(1/0)) ENDなど )、しかしこれは、BooksOnlineからの上記の文章を覚えていない多くの人にとって本当に驚きです。私は、最初にJaime Lafargueから通知を受けたItzikBen-Gan(@ItzikBenGan)によるプライベート電子メール配布リストでの会話で、この特定のシナリオに最初に気づきました。 Connect#690017でバグを報告しました:CASE/COALESCEは常にテキスト順に評価されるとは限りません。 「設計による」として迅速に閉鎖されました。 Paul White(ブログ| @SQL_Kiwi)はその後Connect#691535を提出しました:Aggregates Do n't Follow the Semantics Of CASE、そしてそれは「Fixed」として閉じられました。この場合の修正は、BooksOnlineの記事での説明でした。つまり、上でコピーしたスニペットです。

    この振る舞いは、他のあまり目立たないシナリオでも発生する可能性があります。たとえば、Connect#780132:FREETEXT()は、CASEステートメントの評価の順序を尊重しません(集計は含まれません)。これは、CASEを示しています。 特定のフルテキスト関数を使用する場合も、評価順序が左から右になることは保証されません。その項目について、Paul Whiteは、新しいLAG()を使用して同様のことも観察したとコメントしました。 SQL Server 2012で導入された機能。再現性はありませんが、彼を信じています。これが発生する可能性のあるすべてのエッジケースを発掘したとは思いません。

    したがって、全文検索などの集約または非ネイティブサービスが関係する場合は、CASEでの短絡について何も想定しないでください。 表現。

    RAND()は複数回評価できます

    シンプルを書いている人をよく見かけます CASE このような表現:

    SELECT CASE @variable 
      WHEN 1 THEN 'foo'
      WHEN 2 THEN 'bar'
    END

    これは検索として実行されることを理解することが重要です。 CASE このような表現:

    SELECT CASE  
      WHEN @variable = 1 THEN 'foo'
      WHEN @variable = 2 THEN 'bar'
    END

    評価される式が複数回評価されることを理解することが重要な理由は、実際に評価できるためです。 複数回。これが変数、定数、または列参照である場合、これが実際の問題になる可能性はほとんどありません。ただし、それが非決定論的な関数である場合、状況は急速に変化する可能性があります。この式がSMALLINTを生成すると考えてください。 1から3の間;先に進んで何度も実行すると、常に次の3つの値のいずれかが得られます。

    SELECT CONVERT(SMALLINT, 1+RAND()*3);

    さて、これを単純なCASEに入れてください 式を実行し、それを12回実行します。最終的には、NULLの結果が得られます。 :

    SELECT [result] = CASE CONVERT(SMALLINT, 1+RAND()*3)
      WHEN 1 THEN 'one'
      WHEN 2 THEN 'two'
      WHEN 3 THEN 'three'
    END;

    これはどのように起こりますか?さて、全体のCASE 式は、次のように検索された式に展開されます。

    SELECT [result] = CASE 
      WHEN CONVERT(SMALLINT, 1+RAND()*3) = 1 THEN 'one'
      WHEN CONVERT(SMALLINT, 1+RAND()*3) = 2 THEN 'two'
      WHEN CONVERT(SMALLINT, 1+RAND()*3) = 3 THEN 'three'
      ELSE NULL -- this is always implicitly there
    END;

    次に、各WHENが発生します。 句はRAND()を評価して呼び出します 独立して–そしてそれぞれの場合でそれは異なる値を生み出す可能性があります。式を入力し、最初のWHENをチェックするとします。 節であり、結果は3です。その句をスキップして次に進みます。 RAND()の場合、次の2つの句は両方とも1を返すと考えられます。 が再度評価されます–この場合、どの条件もtrueと評価されないため、ELSE 引き継ぎます。

    他の式は複数回評価できます

    この問題は、RAND()に限定されません。 働き。これらの動くターゲットから来る同じスタイルの非決定論を想像してみてください:

    SELECT 
      [crypt_gen]   = 1+ABS(CRYPT_GEN_RANDOM(10) % 20),
      [newid]       = LEFT(NEWID(),2),
      [checksum]    = ABS(CHECKSUM(NEWID())%3);

    これらの式は、複数回評価された場合、明らかに異なる値を生成する可能性があります。そして、検索されたCASE 式では、すべての再評価が、現在のWHENに固有の検索から外れる場合があります。 、そして最終的にELSEをヒットします 句。これから身を守るための1つのオプションは、常に独自の明示的なELSEをハードコーディングすることです。;均等な分布を探している場合、これはスキュー効果をもたらすため、返すことを選択したフォールバック値に注意してください。もう1つのオプションは、最後のWHENを変更することです。 ELSEへの句 、しかし、これは依然として不均一な分布につながります。私の意見では、推奨されるオプションは、SQL Serverに条件を一度評価するように強制することです(ただし、これは単一のクエリ内で常に可能であるとは限りません)。たとえば、次の2つの結果を比較します。

    -- Query A: expression referenced directly in CASE; no ELSE:
    SELECT x, COUNT(*) FROM
    (
      SELECT x = CASE ABS(CHECKSUM(NEWID())%3) 
      WHEN 0 THEN '0' WHEN 1 THEN '1' WHEN 2 THEN '2' END 
      FROM sys.all_columns
    ) AS y GROUP BY x;
     
    -- Query B: additional ELSE clause:
    SELECT x, COUNT(*) FROM
    (
      SELECT x = CASE ABS(CHECKSUM(NEWID())%3) 
      WHEN 0 THEN '0' WHEN 1 THEN '1' WHEN 2 THEN '2' ELSE '2' END 
      FROM sys.all_columns
    ) AS y GROUP BY x;
     
    -- Query C: Final WHEN converted to ELSE:
    SELECT x, COUNT(*) FROM
    (
      SELECT x = CASE ABS(CHECKSUM(NEWID())%3) 
      WHEN 0 THEN '0' WHEN 1 THEN '1' ELSE '2' END 
      FROM sys.all_columns
    ) AS y GROUP BY x;
     
    -- Query D: Push evaluation of NEWID() to subquery:
    SELECT x, COUNT(*) FROM
    (
      SELECT x = CASE x WHEN 0 THEN '0' WHEN 1 THEN '1' WHEN 2 THEN '2' END 
      FROM 
      (
        SELECT x = ABS(CHECKSUM(NEWID())%3) FROM sys.all_columns
      ) AS x
    ) AS y GROUP BY x;

    配布:

    クエリA クエリB クエリC クエリD
    NULL 2,572
    0 2,923 2,900 2,928 2,949
    1 1,946 1,959 1,927 2,896
    2 1,295 3,877 3,881 2,891

    さまざまなクエリ手法による値の配布

    この場合、SQL Serverがサブクエリの式を評価し、検索されたCASEにそれを導入しないことを選択したという事実に依存しています。 表現ですが、これは単に分布をより均一にするように強制できることを示すためのものです。実際には、これがオプティマイザーの選択であるとは限らないため、この小さなトリックから学ばないでください。 :-)

    CHOOSE()も影響を受けます

    CHECKSUM(NEWID())を置き換えると RAND()を使用した式 式では、まったく異なる結果が得られます。最も注目すべきは、後者は1つの値のみを返すことです。これは、RAND()が原因です。 、GETDATE()のように およびその他のいくつかの組み込み関数は、実行時定数として特別に扱われ、参照ごとに一度だけ評価されます。 行全体に対して。それでもNULLを返すことができることに注意してください 前のコードサンプルの最初のクエリと同じです。

    この問題もCASEに限定されません 表現;同じ基本的なセマンティクスを使用する他の組み込み関数でも同様の動作を確認できます。たとえば、CHOOSE より精巧に検索されたCASEの単なる構文糖衣です 式。これにより、NULLも生成されます。 たまに:

    SELECT [choose] = CHOOSE(CONVERT(SMALLINT, 1+RAND()*3),'one','two','three');

    IIF() これと同じトラップに陥ると予想した関数ですが、この関数は実際には検索されたCASEにすぎません。 考えられる結果が2つだけで、ELSEがない式 –したがって、他の機能をネストしたり導入したりせずに、これが予期せず中断する可能性があるシナリオを想定することは困難です。単純なケースでは、CASEのまともな省略形です。 、2つ以上の可能な結果が必要な場合は、それを使って何か役立つことをするのも難しいです。 :-)

    COALESCE()も影響を受けます

    最後に、そのCOALESCEを調べる必要があります 同様の問題が発生する可能性があります。これらの式は同等であると考えてみましょう:

    SELECT COALESCE(@variable, 'constant');
     
    SELECT CASE WHEN @variable IS NOT NULL THEN @variable ELSE 'constant' END);

    この場合、@variable 2回評価されます(このConnectアイテムで説明されているように、他の関数やサブクエリも同様です)。

    最近のフォーラムディスカッションで次の例を取り上げたとき、私は本当に困惑した表情を得ることができました。テーブルに1〜5の値の分布を入力したいが、3が検出された場合は常に、代わりに-1を使用したいとします。あまり現実的なシナリオではありませんが、構築して従うのは簡単です。この式を書く1つの方法は次のとおりです。

    SELECT COALESCE(NULLIF(CONVERT(SMALLINT,1+RAND()*5),3),-1);

    (英語では、裏返しに作業する:式1+RAND()*5の結果を変換します smallintに;その変換の結果が3の場合は、NULLに設定します。;その結果がNULLの場合 、-1に設定します。より詳細なCASEでこれを書くことができます 表現ですが、簡潔が王様のようです。)

    これを何度も実行すると、-1だけでなく1〜5の範囲の値が表示されるはずです。 3のインスタンスがいくつか表示されます。また、NULLがときどき表示されることに気付いたかもしれません。 、ただし、これらの結果のいずれも期待できない場合があります。分布を確認しましょう:

    USE tempdb;
    GO
    CREATE TABLE dbo.dist(TheNumber SMALLINT);
    GO
    INSERT dbo.dist(TheNumber) SELECT COALESCE(NULLIF(CONVERT(SMALLINT,1+RAND()*5),3),-1);
    GO 10000
    SELECT TheNumber, occurences = COUNT(*) FROM dbo.dist 
      GROUP BY TheNumber ORDER BY TheNumber;
    GO
    DROP TABLE dbo.dist;

    結果(結果は確かに異なりますが、基本的な傾向は似ているはずです):

    TheNumber 発生
    NULL 1,654
    -1 2,002
    1 1,290
    2 1,266
    3 1,287
    4 1,251
    5 1,250

    COALESCEを使用したTheNumberの配布

    検索されたCASE式を分解する

    もう頭をかいてますか?値はどのようにNULL および3が表示され、NULLの分布がなぜであるか と-1実質的に高い?さて、前者に直接答えて、後者の仮説を立てましょう。

    RAND()なので、式は論理的に次のように大まかに展開されます。 NULLIF内で2回評価されます 、次に、COALESCEのブランチごとに2つの評価を掛けます。 働き。私はデバッガーを手元に持っていないので、これは必ずしもSQL Serverの内部で行われることとは*正確に*ではありませんが、要点を説明するのに十分同等である必要があります。

    SELECT 
      CASE WHEN 
          CASE WHEN CONVERT(SMALLINT,1+RAND()*5) = 3 THEN NULL 
          ELSE CONVERT(SMALLINT,1+RAND()*5) 
          END 
        IS NOT NULL 
        THEN
          CASE WHEN CONVERT(SMALLINT,1+RAND()*5) = 3 THEN NULL 
          ELSE CONVERT(SMALLINT,1+RAND()*5) 
          END
        ELSE -1
      END
    END

    したがって、複数回評価されると、すぐに「きみならどうする?」の本になり、NULL および3は、元のステートメントを調べた場合には不可能と思われる可能性のある結果です。興味深い補足:上記の配布スクリプトを使用してCOALESCEを置き換えた場合、これはまったく同じにはなりません。 ISNULLを使用 。その場合、NULLの可能性はありません。 出力;分布はおおまかに次のとおりです。

    TheNumber 発生
    -1 1,966
    1 1,585
    2 1,644
    3 1,573
    4 1,598
    5 1,634

    ISNULLを使用したTheNumberの配布

    繰り返しになりますが、実際の結果は確かに異なりますが、それほど大きくはなりません。重要なのは、3がかなり頻繁にクラックを通過することをまだ確認できるということですが、ISNULL NULLの可能性を魔法のように排除します ずっとやり遂げる。

    COALESCEのその他の違いについて話しました およびISNULL 「SQLServerでのCOALESCEとISNULLのどちらを選択するか」というタイトルのヒント。それを書いたとき、私はCOALESCEを使うことに大いに賛成でした ただし、最初の引数がサブクエリであった場合を除きます(これも、このバグが原因です)。 「機能ギャップ」)。今、私はそれについてそれほど強く感じているかどうかはわかりません。

    単純なCASE式は、リンクされたサーバー上にネストされる可能性があります

    CASEの数少ない制限の1つ 表現は、10のネストレベルに制限されているということです。 dba.stackexchange.comのこの例では、Paul Whiteが(プランエクスプローラーを使用して)次のような単純な式を示しています。

    SELECT CASE column_name
      WHEN '1' THEN 'a' 
      WHEN '2' THEN 'b'
      WHEN '3' THEN 'c'
      ...
    END
    FROM ...

    パーサーによって検索されたフォームに展開されます:

    SELECT CASE 
      WHEN column_name = '1' THEN 'a' 
      WHEN column_name = '2' THEN 'b'
      WHEN column_name = '3' THEN 'c'
      ...
    END
    FROM ...

    ただし、実際には、リンクされたサーバー接続を介して、次のはるかに詳細なクエリとして送信できます。

    SELECT 
      CASE WHEN column_name = '1' THEN 'a' ELSE
        CASE WHEN column_name = '2' THEN 'b' ELSE
          CASE WHEN column_name = '3' THEN 'c' ELSE 
          ... 
          ELSE NULL
          END
        END
      END
    FROM ...

    この状況では、元のクエリにCASEが1つしかない場合でも リンクされたサーバーに送信されたときに、10以上の可能な結果を​​伴う式は、10以上のネストされた CASE 式。そのため、ご想像のとおり、エラーが返されました:

    メッセージ8180、レベル16、状態1
    ステートメントを準備できませんでした。
    メッセージ125、レベル15、状態4
    ケース式はレベル10にのみネストできます。

    場合によっては、Paulが提案したように、次のような式で書き直すことができます(column_nameを想定) varchar列です):

    SELECT CASE CONVERT(VARCHAR(MAX), SUBSTRING(column_name, 1, 255))
      WHEN 'a' THEN '1'
      WHEN 'b' THEN '2'
      WHEN 'c' THEN '3'
      ...
    END
    FROM ...

    場合によっては、SUBSTRINGのみ 式が評価される場所を変更する必要がある場合があります。その他の場合は、CONVERTのみ 。徹底的なテストは実行しませんでしたが、これは、リンクサーバープロバイダー、照合互換性やリモート照合の使用などのオプション、およびパイプの両端にあるSQLServerのバージョンに関係している可能性があります。

    簡単に言うと、CASEを覚えておくことが重要です。 式は警告なしに書き直すことができます。また、現在使用している回避策は、オプティマイザーによって後で無効になる可能性があります。

    CASE式の最終的な考えと追加のリソース

    CASEのあまり知られていない側面のいくつかについて考えるための食べ物を与えたことを願っています 式、およびCASEの状況に関する洞察 –および同じ基本ロジックを使用する一部の関数–は予期しない結果を返します。このタイプの問題が発生した他のいくつかの興味深いシナリオ:

    • スタックオーバーフロー:このCASE式はどのようにしてELSE句に到達しますか?
    • スタックオーバーフロー:CRYPT_GEN_RANDOM()奇妙な効果
    • スタックオーバーフロー:CHOOSE()が意図したとおりに機能しない
    • スタックオーバーフロー:CHECKSUM(NewId())は行ごとに複数回実行されます
    • Connect#350485:NEWID()とテーブル式のバグ

    1. asp.netのSQLサーバーデータベースからの画像を表示するための最良の方法は何ですか?

    2. DatabaseError:現在のトランザクションは中止され、トランザクションブロックが終了するまでコマンドは無視されますか?

    3. OracleのRAWTONHEX()関数

    4. SQLでのテーブルのピボット(クロス集計/クロス集計)