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
式。そのため、ご想像のとおり、エラーが返されました:
ステートメントを準備できませんでした。
メッセージ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()とテーブル式のバグ