はじめに
CASE
式は、SQL-92で標準に取り入れられました。比較的新しい道具であるためか、便利なわりにその真価があまり知られておらず、利用されていなかったり、CASE
式の簡略版であるDECODE
(Oracle)、IF
(MySQL)などの関数で代用されていたりします。
CASE
式を活用すると、SQLでできることの幅がぐっと広がり、書き方もスマートになります。しかも、実装非依存の技術なのでコードの汎用性も高まります。特にDECODE
関数を使っているOracleユーザーには、ぜひCASE
式への乗り換えをお薦めします。
前回『CASE式のススメ(前編)』では、CASE
式の文法とその応用方法について解説しました。今回は引き続き、CASE
式のより高度な応用方法について解説します。
必要な環境
次のいずれかのデータベース(MS Accessは対象外)。
- Oracle(9i以降)
- SQL Server
- DB2
- PostgreSQL
- MySQL
対象読者
入門者~中級者。
条件を分岐させたUPDATE
数値型の列に対して、現在の値を判定対象として別の値へ変えたいというケースを考えます。問題は、そのときのUPDATE
の条件が複数に分岐する場合です。例えば、次のような条件です。
- 現在の給料が30万以上の社員は、10%の減給とする
- 現在の給料が25万以上28万未満の社員は、20%の昇給とする
単純に考えると、次のようにUPDATE
文を2回実行すればよいように思えますが、これは正しくありません。
--条件1 UPDATE Personnel SET salary = salary * 0.9 WHERE salary >= 300000; --条件2 UPDATE Personnel SET salary = salary * 1.2 WHERE salary >= 250000 AND salary < 280000;
というのも、例えば、現在の給料が30万円の社員の場合、当然、条件1のUPDATE
によって給料は27万に減ります。しかし、それで終わりではなく、続いて実行される条件2のUPDATE
によって32万4000円に増えてしまうからです。減給と見えた人事部の仕打ちは、実は2万4000円の昇給だった、ということになります。
もちろん、このような結果は人事部の意図したところではありません。この社員はきっちり27万円に減給せねばなりません。問題は、最初に実行されたUPDATE
によって、「現在の給料」が変わってしまい、正しい条件判定ができないことにあります。だからと言って、実行するSQLの順番を逆にしても、例えば、現在の給料が27万円の社員の場合に同じ問題が発生します。鬼の人事部長の意図を正確に反映するSQLは、次のようにCASE
式を使って書く必要があります。
UPDATE Personnel SET salary = CASE WHEN salary >= 300000 THEN salary * 0.9 WHEN salary >= 250000 AND salary < 280000 THEN salary * 1.2 ELSE salary END;
このSQLは正しいうえに、一度の実行で済むので速度まで速くなります。これなら人事部長も納得でしょう。
なお、最後の行のELSE salary
は非常に重要なので、必ず書いてください。これがないと、条件1と条件2のどちらの条件にも該当しない社員の給料はNULLになってしまいます。これは、「CASE
式に明示的なELSE
句がない場合、デフォルトでNULLが挿入される」という CASE
式の仕様によります。CASE
式を使うときは、常に明示的にELSE
句を書く(たとえNULLでかまわない場合でも!)癖をつけましょう。
このテクニックは応用範囲が広く、これを使えば主キーの値を入れ替えるという荒業も簡単に実現できます。普通、a
とb
という主キーの値を入れ替えるためには、ワーク用の値へ一度どちらかを退避させるか、遅延制約を使う必要があります。前者の方法では3回の UPDATE
が必要になりますが、CASE
式を使えば一つのSQLで実現できます。
主キー(p_key) | 列1(col_1) | 列2(col_2) |
a | 1 | あ |
b | 2 | い |
c | 3 | う |
例えば、上のようなテーブルについて、主キーa
とb
を入れ替えるには、次のように書きます。
UPDATE SomeTable SET p_key = CASE WHEN p_key = 'a' THEN 'b' WHEN p_key = 'b' THEN 'a' ELSE p_key END WHERE p_key IN ('a', 'b');
一読して分かるように、「aならbへ、bならaへ」という条件分岐させたUPDATE
を行なっています。主キーだけでなく、もちろんユニークキーの入れ替えも同様に可能です。ポイントは先ほどの昇給・減給の例題のときと同じです。すなわち、CASE
式の分岐による更新は「一気に」行なわれるので、主キーの重複によるエラーを回避できるのです。
ただし、このような入れ替えをする必要が生じるということは、テーブル設計にどこか間違いがある可能性が高いので、まずは設計を見直して、必要がなければ制約を外してください。
テーブル同士のマッチング
CASE
式はDECODE
関数と違って式を評価できます。それはつまり、CASE
式の中でBETWEEN
、LIKE
、IS NULL
といった便利な述語群を使用できるということです。中でもIN
とEXISTS
はサブクエリを作れるため、非常に強力な表現力を持ちます。CASE
式の中でこの2つを使うことで、サブクエリをSELECT
句で書くことができます。
さて、テーブル「tbl_A」と「tbl_B」をkeyCol
列でマッチングすることを考えます。すると、テーブル「tbl_B」とマッチするキーを持つテーブル「tbl_A」のレコードに「Matched」、マッチしないレコードに「Unmatched」というラベルを貼るSQLは次のように記述できます。
--IN述語の場合 SELECT keyCol, CASE WHEN keyCol IN ( SELECT keyCol FROM tbl_B ) THEN 'Matched' ELSE 'Unmatched' END Label FROM tbl_A; --EXISTS述語の場合 SELECT keyCol, CASE WHEN EXISTS ( SELECT * FROM tbl_B WHERE tbl_A.keyCol = tbl_B.keyCol ) THEN 'Matched' ELSE 'Unmatched' END Label FROM tbl_A;
IN
とEXISTS
どちらを使っても、結果は同じになります。同様にNOT IN
とNOT EXISTS
を使って「マッチしない」という条件を書くこともできますが、その場合はサブクエリ内で参照されるテーブルの列にNULLが存在するか否かで両者の動作が異なることに注意が必要です。この問題については、筆者のWebサイトの『3値論理』を参照してください。