SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

DeveloperZine(デベロッパージン)- エンジニアの意思決定を支える技術情報メディア ProductZine

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

達人に学ぶSQL

外部結合の使い方

結果のフォーマット整形と集合演算を外部結合で行う


ダウンロード サンプルソース (2.4 KB)

外部結合で行列変換 その2(列→行):繰り返し項目を1列にまとめる

 前問では行から列へ変換しました。それなら今度は、列から行へ変換したくなるのが人情というもの。例えば次のようなDBエンジニア泣かせのテーブルを考えましょう。

Personnel:社員の子供情報
社員(employee) 子供1(child_1) 子供2(child_2) 子供3(child_3)
赤井 一郎 二郎 三郎
工藤 春子 夏子  
鈴木 夏子    
吉田      

 皆さんも、1度は目にしたことがあるでしょう。COBOLなどで使われているファイルを入力データとする場合に、安易にそのフォーマットに引きずられると、こういうテーブルが出来上がります。このテーブルのどの辺が泣かせどころかという点には、今は立ち入りません。こういうテーブルは「行持ち」の形式へ変換するのが基本です。最も簡単なやり方は、UNION ALLを使います。

列から行への変換:UNION ALLの利用
SELECT employee, child_1 AS child FROM Personnel
UNION ALL
SELECT employee, child_2 AS child FROM Personnel
UNION ALL
SELECT employee, child_3 AS child FROM Personnel;
結果
employee    child
----------  -------
赤井        一郎
赤井        二郎
赤井        三郎
工藤        春子
工藤        夏子
工藤
鈴木        夏子
鈴木
鈴木
吉田
吉田
吉田

 UNION ALLは重複行を排除しないので、子供のいない吉田氏についてもきっちり3行出力されます。テーブルへ格納するならchild列がNULLの行を排除した形にするのが良いでしょう。

 また時として、子供のいない吉田氏も残した次のようなリストが欲しい場合もあります。

社員の子供リスト
社員(employee) 子供(child)
赤井 一郎
赤井 二郎
赤井 三郎
工藤 春子
工藤 夏子
鈴木 夏子
吉田  

 このケースでは、単純にchild列がNULLの行を除外するわけにはいきません。方法はいくつか考えられますが、ここでは社員一覧をマスタとした外部結合を使いましょう。結合条件に注目してください。

社員の子供リストを得るSQL(子供のいない社員も出力する)
SELECT EMP.employee, CHILDREN.child
FROM   Personnel EMP
  LEFT OUTER JOIN
   (SELECT child_1 AS child FROM Personnel
    UNION
    SELECT child_2 AS child FROM Personnel
    UNION
    SELECT child_3 AS child FROM Personnel) CHILDREN
  ON CHILDREN.child IN (EMP.child_1, EMP.child_2, EMP.child_3);

 インライン・ビュー「CHILDREN」は、子供全員を含む集合、いわば「子供マスタ」です(例によって、この集合が最初からテーブルとして用意されているなら、それを使ってください)。それと社員テーブルを外部結合しているわけですが、重要なのは結合条件をIN述語で指定していることです。これによって、「Personnel」テーブルの子供1~子供3の列に「CHILDREN」ビューの名前と一致する子供がいればその名前が、一致しなければNULLが返ります。工藤家と鈴木家には同名の子供「夏子」がいますが、その場合でも正しく動作します。

クロス表で入れ子の表側を作る

 統計表を作成する業務では、表側や表頭を入れ子にした表を作りたいという要望がよく発生します。例えば、県別・年齢階級別・性別の人口データを保持する「TblPop」テーブルから、次のようなクロス表を作るケースです。

年齢階級マスタ:TblAge
年齢階級(age_class) 年齢(age_range)
1 21~30歳
2 31~40歳
3 41~50歳
性別マスタ:TblSex
性別コード(sex_cd) 性別(sex)
m
f
人口構成テーブル:TblPop
県名(pref_name) 年齢階級(age_class) 性別コード(sex_cd) 人口(population)
秋田 1 m 400
秋田 3 m 1000
秋田 1 f 800
秋田 3 f 1000
青森 1 m 700
青森 1 f 500
青森 3 f 800
東京 1 m 900
東京 1 f 1500
東京 3 f 1200
千葉 1 m 900
千葉 1 f 1000
千葉 3 f 900
 
表側が入れ子の統計表
    東北 関東
21~30歳 1100 1800
1300 2500
31~40歳    
   
41~50歳 1000  
1800 2100

 問題の要点は、「TblPop」テーブルには年齢階級「2」のデータが1行もないのだけれど、結果にはその階級も含めて、6行固定で出力することです。表側固定となれば外部結合の出番ですが、表側を入れ子にするには一ひねり必要です。今回は年齢階級と性別が表側なので、「TblAge」と「TblSex」をマスタに使います。

 基本的な考え方としては、この2つのテーブルをマスタにして外部結合するのですが、単純に外部結合を繰り返すだけではうまくいきません。

外部結合で入れ子の表側を作る:間違ったSQL
SELECT 
  MASTER1.age_class AS age_class,
  MASTER2.sex_cd    AS sex_cd,
  DATA.pop_tohoku   AS pop_tohoku,
  DATA.pop_kanto    AS pop_kanto
FROM
  (SELECT 
     age_class,
     sex_cd,
     SUM(CASE WHEN pref_name IN ('青森', '秋田')
              THEN population ELSE NULL END) AS pop_tohoku,
     SUM(CASE WHEN pref_name IN ('東京', '千葉') 
              THEN population ELSE NULL END) AS pop_kanto
     FROM TblPop
   GROUP BY age_class, sex_cd) DATA
   RIGHT OUTER JOIN TblAge MASTER1 
                                     --外部結合1:年齢階級マスタと結合
     ON MASTER1.age_class = DATA.age_class
       RIGHT OUTER JOIN TblSex MASTER2
                                     --外部結合2:性別マスタと結合
         ON MASTER2.sex_cd = DATA.sex_cd;
結果
age_class    sex_cd    pop_tohoku    pop_kanto
-----------  --------  ------------  -----------
1            m         1100          1800
1            f         1300          2500
3            m         1000
3            f         1800          2100

 結果を見ての通り、年齢階級「2」の行が現れません。これは困りました。せっかく外部結合を使ったのに、なぜうまくいかないのでしょう? それは、「TblPop」テーブルに年齢階級「2」のレコードが1行もなかったからです。……あれ、でも外部結合ってそういう場合でも定型的な結果を得るための技術じゃなかったっけ?

 そう、確かにそのとおりです。その証拠に、最初の年齢階級マスタとの結合を終えた時点では、年齢階級「2」のレコードもちゃんと結果に含まれているのです。

最初の外部結合で止めた場合:年齢階級「2」も結果に現れる
SELECT
  MASTER1.age_class AS age_class,
  DATA.sex_cd       AS sex_cd,
  DATA.pop_tohoku   AS pop_tohoku,
  DATA.pop_kanto    AS pop_kanto
FROM
  (SELECT
     age_class,
     sex_cd,
     SUM(CASE WHEN pref_name IN ('青森', '秋田') 
              THEN population ELSE NULL END) AS pop_tohoku,
     SUM(CASE WHEN pref_name IN ('東京', '千葉') 
              THEN population ELSE NULL END) AS pop_kanto
     FROM TblPop
   GROUP BY age_class, sex_cd) DATA
     RIGHT OUTER JOIN TblAge MASTER1
     ON MASTER1.age_class = DATA.age_class;
結果
age_class    sex_cd    pop_tohoku    pop_kanto
-----------  --------  ------------  -----------
1            m         1100          1800
1            f         1300          2500
2                                            --年齢階級「2」も存在する
3            m         1000
3            f         1800          2100

 しかし、です。ここが核心なので注意してほしいのですが、年齢階級「2」は確かに「TblAge」から取得できます。しかし、それに対応する「TblPop」テーブルの性別コードはNULLになるのです。これは考えてみれば当然のことで、「TblPop」テーブルに年齢階級「2」のデータは存在しないのだから、その性別に「m」も「f」もありません。NULL以外は入りようがないのです。そのため、次の性別マスタとの外部結合において、結合条件が「ON MASTER2.sex_cd = NULL」となってしまい、結果はunknownです(この真理値の意味は『3値論理とNULL』を参照)。従って、最終結果には絶対に年齢階級「2」の行が現れません。結合するテーブルの順番を逆にしてもうまくいきません。

 では、どうすれば入れ子の表側を正しく作れるでしょう。答えは、外部結合を2度することが許されないなら、1度で済ませてしまえばよい、というものです。

外部結合で入れ子の表側を作る:正しいSQL
SELECT
  MASTER.age_class AS age_class,
  MASTER.sex_cd    AS sex_cd,
  DATA.pop_tohoku  AS pop_tohoku,
  DATA.pop_kanto   AS pop_kanto
FROM
  (SELECT
     age_class,
     sex_cd,
     SUM(CASE WHEN pref_name IN ('青森', '秋田')
              THEN population ELSE NULL END) AS pop_tohoku,
     SUM(CASE WHEN pref_name IN ('東京', '千葉')
              THEN population ELSE NULL END) AS pop_kanto
   FROM TblPop
   GROUP BY age_class, sex_cd) DATA
     RIGHT OUTER JOIN
       (SELECT age_class, sex_cd
          FROM TblAge 
                CROSS JOIN    --クロス結合でマスタ同士の直積を作る
               TblSex ) MASTER
     ON  MASTER.age_class = DATA.age_class
    AND  MASTER.sex_cd    = DATA.sex_cd;
結果
age_class    sex_cd    pop_tohoku    pop_kanto
-----------  --------  ------------  -----------
1            m         1100          1800
1            f         1300          2500
2            m
2            f
3            m         1000
3            f         1800          2100

 これでしっかり6行が得られました。「TblPop」がどれほど不完全なテーブルでも、結果の表側は常に6行固定で得られます。トリックは「TblAge」と「TblSex」をクロス結合して、「MASTER」という直積を作ることです。

MASTER
年齢階級(age_class) 性別コード(sex_cd)
1 m
1 f
2 m
2 f
3 m
3 f

 すると、外部結合はこの「MASTER」ビューに対する1度だけで済みます。つまり、表側を入れ子にするときは、その形のマスタをあらかじめ用意してやればよいのです。3レベル以上の入れ子の場合も、同じやり方で拡張できます。

 なお、CROSS JOIN構文を持っていないDBMSの場合は、「FROM TblAge, TblSex」のように、結合条件の指定なしでテーブルを並べればクロス結合と同じ演算になります。

次のページ
完全外部結合

この記事は参考になりましたか?

達人に学ぶSQL連載記事一覧

もっと読む

この記事の著者

ミック(ミック)

日本では、主にBI/DWHの設計からチューニングまでを専門とするデータベースエンジニアとして活動。2018年より米国シリコンバレーに活動拠点を移し、技術調査とビジネス開発に従事している。主な著書・訳書:『達人に学ぶSQL徹底指南書 第2版』(2018)『SQL実践入門』(2015)Joe Celko『プログラマのためのSQL 第4版』(2015)翔泳社 - 著者ページ:https://www.shoeisha.co.jp/book/author/3964著者個人ページ:http://mickindex.sakura.ne.jp/

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

CodeZine(コードジン)
https://codezine.jp/article/detail/751 2008/08/22 19:38

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー