Posted by & filed under ORACLE.

CASE式のススメ:

CASE式のススメ

 CASE式は SQL-92 で標準に取り入れられた、割と新しい道具です。そのため、便利な割に真価があまり知られておらず、利用されていなかったり、CASE式の簡略版である DECODE や COALESCE などの関数で代用されていたりします。しかしセルコが「SQL-92 で追加された中で最も有用かもしれない」[1]と言うように、CASE式を活用すると SQL でできることの幅がぐっと広がり、書き方もスマートになります。特に、DECODE関数を使っている Oracle ユーザには、是非 CASE式への乗り換えを勧めます[2]
 以下では、CASE式の魅力の一端を解説します。例によって『プログラマのためのSQL 第2版』から多くを受け売っています。

 稼働環境:Oracle(9i以降)、SQLServer、DB2、PostgreSQL、MySQL
  

0.導入:CASE式とは
1.既存のコード体系を新しい体系に変換して集計する
2.異なる条件の集計を一つのSQLで行なう
3.CHECK制約で二つの列の相関関係を定義する
4.条件を分岐させたUPDATE
5.テーブル同士のマッチング
6.CASE式の中で集約関数を使う
7.やってはいけない間違い

0.導入:CASE式とは

 まず基本的な文法から解説しましょう。CASE式の書式には、単純CASE式(simple case)と検索CASE式(searched case)という二通りがあります。それぞれ、次のように書きます。

     –単純CASE式
     CASE sex
     WHEN ‘1’ THEN ‘男’
     WHEN ‘2’ THEN ‘女’
     ELSE ‘その他’ END

     –検索CASE式
     CASE WHEN sex = ‘1’ THEN ‘男’
         WHEN sex = ‘2’ THEN ‘女’
     ELSE ‘その他’ END

この二つは、どちらも同じ動作をします。「性別(sex)」列が ‘1’ なら「男」へ、’2′ なら「女」へ読み替えているわけです。単純 CASE 式の方が、その名の通り簡潔に書けますが、できることも限られています。単純 CASE 式で書ける条件は、検索 CASE 式でも書くことができます。以下でも、検索 CASE 式の方を多用します。
 また、CASE 式の評価は、真になる WHEN 句が見つかった時点で打ち切られて、残りの WHEN 句は無視されるので、そのことを意識してコーディングする必要があります。混乱を避けるためにも WHEN 句は排他的に記述するのが良いでしょう。

     –例えば、こんなふうに書くと、結果には「2番」が現れない
     CASE WHEN col_1 IN (‘a’, ‘b’)  THEN ‘1番’
         WHEN col_1 IN (‘a’)    THEN ‘2番’
     ELSE ‘その他’ END

1.既存のコード体系を新しい体系に変換してGROUP BY

 非定型的な集計を行なう業務では、既存のコード体系を分析用のコード体系に変換して、その新体系の単位で集計したい、という要件が持ち込まれることがあ ります。例えば、県コードは、1:北海道、2:青森、・・・・・・ 47:沖縄というように振られていますが、これを東北、関東、九州といった地方単位にまとめて、その単位で人口を集計したい、という場合です。
 こんなとき、皆さんならどうしますか? 「地方コード」という列を持つビューを定義する、というのも一つの方法です。しかしそれだと、集計に使いたいコード体系の数だけ列を追加せねばなりませんし、動的な変更も困難です。
 CASE式を使うと次のような一つのSQLで可能です。ここでは分かりやすくするためにコードの代わりに県名で GROUP BY します。

     SELECT SUM(popularity),
           CASE pre_name
               WHEN ‘徳島県’ THEN ‘四国’
               WHEN ‘香川県’ THEN ‘四国’
               WHEN ‘愛媛県’ THEN ‘四国’
               WHEN ‘高知県’ THEN ‘四国’
               WHEN ‘福岡県’ THEN ‘九州’
               WHEN ‘佐賀県’ THEN ‘九州’
               WHEN ‘長崎県’ THEN ‘九州’
               WHEN ‘熊本県’ THEN ‘九州’
               WHEN ‘大分県’ THEN ‘九州’
               ELSE ‘その他’ END
     FROM Table_A
     GROUP BY CASE pre_name
               WHEN ‘徳島県’ THEN ‘四国’
               WHEN ‘香川県’ THEN ‘四国’
               WHEN ‘愛媛県’ THEN ‘四国’
               WHEN ‘高知県’ THEN ‘四国’
               WHEN ‘福岡県’ THEN ‘九州’
               WHEN ‘佐賀県’ THEN ‘九州’
               WHEN ‘長崎県’ THEN ‘九州’
               WHEN ‘熊本県’ THEN ‘九州’
               WHEN ‘大分県’ THEN ‘九州’
               ELSE ‘その他’ END;

 豪快に GROUP BY句にも SELECT句の CASE式をコピーしてやるのがポイントです。単純に「GROUP BY pre_name」と変換前の列を指定すると、正しい結果が得られませんので注意してください。
 また、同様の考え方で、数値型の列を適当な階級体系に振り分けて集計することも可能です。例えば、所得階級(salary_class)ごとのレコード数を調べたい場合は、次のような クエリ になります。

     SELECT
        CASE WHEN salary <= 500 THEN ‘1’
            WHEN salary > 500 AND salary <= 600 THEN ‘2’
            WHEN salary > 600 AND salary <= 800 THEN ‘3’
            WHEN salary > 800 AND salary <= 1000 THEN ‘4’
        ELSE NULL END salary_class,
        COUNT(*)
     FROM Table_A
     GROUP BY
        CASE WHEN salary <= 500 THEN ‘1’             –SELECT句のCASE式と同じ!
            WHEN salary > 500 AND salary <= 600 THEN ‘2’
            WHEN salary > 600 AND salary <= 800 THEN ‘3’
            WHEN salary > 800 AND salary <= 1000 THEN ‘4’
        ELSE NULL END;

2.異なる条件の集計を一つのSQLで行なう

 CASE式の使い方として有名なものの一つです。例えば、あるテーブルの男性と女性の人数の部署ごとの合計を求めるとき、普通は次のように二回SQLを発行します。

     –男性の人数
     SELECT dept_nbr,
           COUNT(*)
     FROM Table_A
     WHERE SEX = ‘1’;
     GROUP BY dept_nbr;

     –女性の人数
     SELECT dept_nbr,
           COUNT(*)
     FROM Table_A
     WHERE SEX = ‘2’;
     GROUP BY dept_nbr;

 UNIONを使えば一つの SQL にできますが、コストは変わりません。一方、CASE式を使えば、次のように一つの SQL で済みます。

     SELECT dept_nbr,
           SUM( CASE WHEN sex = ‘1’ THEN 1 ELSE 0 END),    –男性の人数
           SUM( CASE WHEN sex = ‘2’ THEN 1 ELSE 0 END)     –女性の人数
     FROM Table_A
     GROUP BY dept_nbr;

 性別が男性(1)のレコードと女性(2)のレコードの件数を、それぞれ数えているわけです。いわば「行持ち」のデータを「列持ち」に変換 しているのです。恐らくは速度もこちらのが速いでしょう。集約関数であれば、SUM に限らず COUNT でも AVG でも同様に使えます。
 このトリックの重宝するところは、SQL の結果を2次元表の形に整形できることです。単純に GROUP BY で集約しただけだと、その後でプログラムや Excel 上でピボット表の形式に変換せねばなりません。しかし、上の例を見ると、表側が部署番号、表頭が性別という、既に2次元表の形式で結果が出力されることが分かります。これは集計表を作るときに非常に便利な機能です。この技をスローガン的に表現するならば、

  WHERE句で条件分岐させるのは素人のやること。プロはSELECT句で分岐させる

ということです。使いでのある技なので、多用してください。

3.CHECK制約で二つの列の条件関係を定義する

 実は、というほどでもないのですが、CASE式は CHECK制約と非常に相性が良いのです。実際に CASE式を使う局面の半分は CHECK制約の中ではないかと思うほどです。あまり CHECK制約を使わない DBエンジニアも多いかもしれませんが、CASE式と組み合わせたときの表現力の強さを知れば、きっとすぐに利用したくなるでしょう。
 さて、「女性社員の給料は20万円以下」という給与体系を持つ会社があるとします。この言語道断な会社の人事テーブルにおいて、この条件を CHECK制約で表現します。

     CONSTRAINT check_salary CHECK
           ( CASE WHEN sex = ‘2’
                THEN CASE WHEN salary <= 200000
                        THEN 1 ELSE 0 END
                ELSE 1 END = 1 )

 CASE式を入れ子にして「社員の性別が女性ならば、給料は20万円以下である」という命題を表現しています。これは命題論理で条件法 (conditional)と呼ばれる論理式で、形式的に書けば「P → Q」となります。ここで一つ、非常に重要なことを理解してください。それは、条件法と論理積(logical puroduct)との違いです。論理積とは「P かつ Q」を意味する論理式で形式的には「P ∧ Q」と書きます。CHECK制約で表現すると以下のようになります。

     CONSTRAINT check_salary CHECK
           ( sex = ‘2’ AND salary <= 200000 )

 この二つの制約は、もちろん異なる動作をします。では、一体どのように異なるのでしょうか? 解答と解説はここです[3]

4.条件を分岐させたUPDATE

 数値型の列に対して、現在の値を判定対象としてを別の値へ変えたい、というケースを考えます。問題は、そのときの UPDATE の条件が複数に分岐する場合です。例えば、次のような条件です。

  1.現在の給料が30万以上の社員は、10%の減給とする
  2.現在の給料が25万以上28万未満の社員は、20%の昇給とする

 単純に考えると、次のように UPDATE文を2回実行すればよいように思えますが、これは正しくありません。

     –条件1.
     UPDATE Personnel
     SET salary = salary * 0.9
     WHERE salary >= 300000;

     –条件2.
     UPDATE Personnel
     SET salary = salary * 1.20
     WHERE salary >= 250000 AND salary < 280000;

 というのも、例えば、現在の給料が30万円の社員の場合、当然1.の UPDATE によって給料は27万へと減ります。しかしそれで終わりではなく、2.の UPDATE によって32万4000円に増えるのです。減給と見えた人事部の仕打ちは、実は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.20
                  ELSE salary END;

 このSQLは正しいうえに一度の実行で済むので速度まで速くなります。これなら人事部長も納得でしょう。
 なお、最後の行の「ELSE salary」は非常に重要ですので、必ず書いてください。これがないと、1.と2.のどちらの条件にも該当しない社員の給料はNULLになってしまいます。これは、CASE式に明示的なELSE句がない場合、データベースがデフォルトでNULLを挿入するからです。CASE式を使うときは常に明示的にELSE句を書く(たとえNULLでかまわない場合でも!)癖をつけましょう。
 このトリックは応用範囲が広く、これを使えば主キーの値を入れ替えるという荒技も簡単に実現できます。 普通、a と b という主キー値を入れ替えるためには、ワーク用の値へ一度どちらかを退避させるか、遅延制約を使わねばなりません。前者の方法など3回の UPDATE が必要になります。しかし、CASE式を使えば一つの SQL で実現できます。

SomeTable
主キー
(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式の分岐による更新は「一気に」行なわれるので、主キーの重複によるエラーを回避できるのです。
 ただし、このような入れ替えをする必要が生じるということは、テーブル設計にどこか間違いがある可能性が高いので、まずは設計を見直して、必要なければ制約を外してください。

5.テーブル同士のマッチング

 CASE式は DECODE関数と違って式を評価できます。それはつまり、CASE式の中で BETWEEN、LIKE、IS NULL といった便利な述語群を使うことができるということです。中でも IN と EXISTS はサブクエリを作れるため、非常に強力な表現力を持ちます。CASE式の中でこの二つを使うことで、サブクエリを SELECT句で書くことができます。
 さて、tbl_A と tbl_B を keyCol 列でマッチングすることを考えます。すると、tbl_B とマッチするキーを持つ tbl_A のレコードに「Match」、マッチしないレコードに「Unmatch」というラベルを貼る SQL は次のように書けます。

     –IN述語の場合
     SELECT keyCol,
        CASE WHEN keyCol IN ( SELECT keyCol From tbl_B )
            THEN ‘Match’
            ELSE ‘Unmatch’ END Label
     FROM tbl_A;

     –EXISTS述語の場合
     SELECT keyCol,
        CASE WHEN EXISTS ( SELECT * From tbl_B WHERE tbl_A.keyCol = tbl_B.keyCol )
            THEN ‘Match’
            ELSE ‘Unmatch’ END Label
     FROM tbl_A;

 IN と EXISTS どちらを使っても、結果は同じになります。同様に NOT IN と NOT EXISTS を使って「マッチしない」という条件を書くこともできますが、その場合は参照される側のテーブルに NULL が存在するか否かで両者の動作が異なることに注意が必要です。この問題については「3値論理」を参照してください。

 MySQL ユーザへの注意:この SQL は、サブクエリのサポートされたバージョン4.1以降でのみ使用可能です。

6.CASE式の中で集約関数を使う

 これはちょっと高度な使い方です。一見すると文法エラーに見えますが、そうではありません。例として、次のような開発チームのメンバーと特技のテーブルを考えます。主キーは { 社員番号, 特技ID } です。多対多の関係を扱うための関連エンティティの構造です。

Team
社員番号
(emp_id)
特技ID
(skill_id)
職位
(class)
特技名
(skill_name)
主な特技フラグ
(main_skill_flg)
100 1 営業 顧客寝返りの術 Y
100 2 営業 Excel乱れ突き N
200 2 SE Excel乱れ突き N
200 3 SE 仕様変更乱舞 Y
200 4 SE 極限残業法 N
300 4 プログラマ 極限残業法 N
400 5 プログラマ こっそりメモリリーク N
500 6 プログラマ うっかり無限ループ N

 「チーム:俺たちに明日はない」とでも命名したくなるような世紀末的チームですが、要するにメンバーの特技を一覧するテーブルです。複数持っている社員 (100, 200) もいれば、一つしか持っていない社員 (300,400,500) もいます。複数持っている社員については、主な特技がどれかを示すフラグ列に Y または N の値が入ります。一つだけの特技を持つ社員の場合は、N が入ります。
 さて、このテーブルから、次のような条件でクエリを発行します。

  1.特技を一つだけ持つ社員については、その特技IDを取得する
  2.特技を複数持つ社員については、主な特技のIDを取得する

 単純に考えれば、次のような二つの条件に対応するクエリを発行すればよいと思われます。「特技を一つだけ持つか、複数持つか」は、集計結果に対する条件なので HAVING句を使います[4]

     –条件1.
     SELECT emp_id, MAX(skill_id)
     FROM Team
     GROUP BY emp_id
     HAVING COUNT(*) = 1;

結果1:
EMP_ID MAIN_SKILL
—— ———-
300       4
400       5
500       6

     –条件2.
     SELECT emp_id, skill_id
     FROM Team
     WHERE main_skill_flg = ‘Y’
     AND  emp_id IN ( SELECT
                FROM Team
                GROUP BY emp_id
                HAVING COUNT(*) > 1
         );

結果2:
EMP_ID MAIN_SKILL
—— ———-
100       1
200       3

 確かにこれでもできるのですが、例によって複数の SQL が必要となり、実行コストもかかります。CASE式を使えば次のような一つの SQL で書けます。

     SELECT emp_id,
           CASE WHEN COUNT(*) = 1  –特技を一つだけ持つ社員の場合
               THEN MAX(skill_id)
               ELSE MAX(CASE WHEN main_skill_flg = ‘Y’
                          THEN skill_id
                          ELSE NULL END
                   )
           END main_skill
     FROM Team
     GROUP BY emp_id;

結果:
EMP_ID MAIN_SKILL
—— ———-
100       1
200       3
300       4
400       5
500       6

 CASE 式の中に集約関数を書いてさらにその中に CASE 式を書くという、眩暈のしそうな入れ子構造ですが、要するにやりたかったことは、「特技を一つだけ持つのか、複数持つのか」という条件分岐を「CASE WHEN COUNT(*) = 1 …… ELSE ……」という CASE 式で実現することです。これはちょっと革命的な書き方です。なぜなら、私たちは SQL 入門の手ほどきを受けるとき、集計結果に対する条件は HAVING 句を使って設定すると習いますが、CASE 式を使えば SELECT 句でも同様の分岐が書けるからです。この技をスローガン的に表現するならば、

  HAVING 句で条件分岐させるのは素人のやること。プロは(以下略)

 となります。この例題からも分かるように、CASE 式は SELECT 句で集約関数の中にも外にも書くことができます。この自由度の高さが CASE 式の大きな魅力です。

7.やってはいけない間違い

 CASE 式を使うとき、絶対にやってはいけない、しかし初心者がやってしまいがちな間違いを紹介しておきましょう。それは、やはりと言うべきでしょうか、NULL が絡むときの問題です。
 次の CASE 式を見てください。

     CASE col_1
     WHEN 1         THEN ‘○’
     WHEN NULL  THEN ‘×’
     ELSE NULL END

 意図していることは明らかです。col_1 が 1 ならば「○」を、NULL ならば「×」を返したいわけです。確かに、col_1 が 1 の場合は、問題なく「○」が返ります。しかしこの CASE 式が「×」に評価されることは、決してありません。二番目の WHEN 句が常に unknown になってしまうからです。結局のところ、この WHEN 句が、「WHEN col_1 = NULL」の簡略版に過ぎないことを忘れないでください。正しく動作させるためには、次のように記述します。

     CASE WHEN col_1 = 1          THEN ‘○’
         WHEN col_1 IS NULL  THEN ‘×’
     ELSE NULL END

 もっとも、一番良い方針が、テーブル設計の段階で col_1 に NOT NULL 制約をつけておくことであることは、言うまでもありません。そうすれば、WHEN 句の条件を記述するときに無用の間違いを回避することができます。 x

おわりに

 最後に、少し細かい話をします。CASE「式」であって CASE「文」ではないので、間違えないようにしてください。SELECT「文」や UPDATE「文」のような、一つの実行の単位ではなく、「1 + 1」や「a / b」と同じ式の仲間なので、実行時には評価されて一つの値になります。手続き型言語の CASE文と混同しやすいのですが、別物なので注意してください。SQL はそのような手続きと縁を切ることで成立した言語です。
 それでは、この文章を読んで、いっちょう現場で CASE式を使ってみるか、という気になってもらえたら幸いです。理屈を理解したら、後はひたすら書いて書いて書きまくることだけが上達の道です。健闘を祈ります。


[1] J.セルコ『プログラマのためのSQL 第2版』(ピアソン・エデュケーション 2001) p.117
 私は同書から、CASE 式に限らず、SQL とデータベースについて多くのことを学びました。このテキストは詰まるところ、この本のへの入門ないしは解説として書かれたものです。未読の方には、一読をすすめます。

[2]DECODE 関数が CASE 式に比べて劣っていると思う点は、次の4つです。第一に、Oracle の方言なので互換性がありません。第ニに、分岐の数が127に限定されています(引数の上限数は255ですが、一つの分岐を表現するのに二つの引数を要し ます)。第三に、分岐の数が増えるとソースが非常に読みづらくなります。第四に、記述力が貧弱です。具体的には、引数に述語を使った式を取ることができま せん。当然、サブクエリを作ることもできません。 ×
     SELECT DECODE( col_1 < 3, true, 0)
     FROM SomeTable;

     –2.CASE式による例 
     SELECT CASE WHEN col_1 < 3 THEN 0
           END
     FROM SomeTable; –>

[3] 正解:論理積の CHECK制約を付けると、この会社は男性を雇用できなくなる。条件法であれば、男性も働ける。
 解説:論理積「P ∧ Q」を満たす場合は、命題 P と 命題 Q が共に真か、どちらかが真でもう一方が不明である場合です。つまりこの会社で働けるのは「女性であり、かつ、給料が20万円以下」の社員か、性別または給 料の値が不明の社員の場合です(どちらかの条件が偽になるなら、もう片方の条件が不明な人でも働けません。)
 一方、条件法「P ならば Q」を満たす場合は、P と Q が共に真の場合と、P が偽または不明な全ての場合です。以下に両者の真理値表を示すので参考にしてください。なお、U は、SQL の3値論理に特有の真理値 unknown の略です。(3値論理については、私の「3値論理」を参照してください。)

論理積
P Q P ∧ Q
T T T
T F F
T U U
F T F
F F F
F U F
U T U
U F F
U U U
条件法
P Q P → Q
T T T
T F F
T U F
F T T
F F T
F U T
U T T
U F T
U U T

 見てのとおり、条件法はそもそも社員の性別が女性でない(あるいは性別が分からない)場合は真となります。その意味で、論理積よりも緩い制約である、と言えます。

[4]条件1.の SQL の skill_id になぜ MAX関数を使っているかというと、skill_id を GROUP BY のキーに含められないため、文法エラーを防ぐための便宜的措置です。だから別にここは MIN でもかまいません。どうせ一行しかないので集計結果は同じです。ただし、SUM や AVG などの集約関数を使うことは避けた方が良いでしょう。数値型にしか使えないからです。
 SQLが提供する二つの極値関数 MAX と MIN は、数値型でも文字型でも日付型でも使用することが可能です。


Copyright (C) ミック
作成日:2005/03/25
最終更新日:2006/05/22

Comments are closed.