ログイン記事を投稿する会員登録はこちら!ヘルプ

★★★★★

自己結合の使い方

[初級〜中級] 集合指向言語としてのSQL
ミック [著]  | 
評価:4.58 σ = 1.12
公開:06/07/31
Web
DB
EZ新着
SQLが提供する結合演算は、通常、異なるテーブルまたはビューを対象として行われますが、同一のテーブルを対象とした「自己結合」を行うこともできます。自己結合は、動作がイメージしにくいため敬遠されがちですが、使いこなせば非常に便利な技術です。本稿では、この自己結合について例題をもとに分かりやすく解説します。
サンプルファイル 0.9KB (133)
Page 1 / 2

部分的に不一致なキーの検索

 次のような住所録テーブルを考えます。主キーは個人名で、同じ家族の人間は家族IDも一致します。年賀状用などでこういう住所録を作っている人も、結構いると思います。

Addresses
名前(name)家族ID(family_id)住所(address)
前田 義明100東京都港区虎ノ門3-2-29
前田 由美100東京都港区虎ノ門3-2-92
加藤 茶200東京都新宿区西新宿2-8-1
加藤 勝200東京都新宿区西新宿2-8-1
ホームズ300ベーカー街221B
ワトソン400ベーカー街221B

 基本的に、同じ家族であれば同じ住所に住んでいますが(例:加藤家)、ホームズとワトソンのように、家族ではないけど同居しているカップルもいます。さて、前田夫妻に注目です。別に二人は別居中なのではなく、単に夫人の住所が間違っているだけです。本当は、家族IDが同じなら住所も同じでなくてはなりません。これは修正が必要です。では、前田夫妻のような「同じ家族だけど住所が不一致なレコード」を検出するにはどうすればよいでしょう?

 いくつかの方法が考えられますが、ここでも自己非等値結合を使うと簡潔に書けます。

--同じ家族だけど、住所が違うレコードを検索するSQL
SELECT DISTINCT A1.name, A1.address
  FROM Addresses A1,
       Addresses A2
 WHERE A1.family_id = A2.family_id
   AND A1.address <> A2.address ;

 「同じ家族で、かつ、住所が違う」をSQLに逐語訳しただけなので、意味的に悩む箇所はないと思います。このように、自己結合と非等値結合の組み合わせは、実に強力です。またこのSQLは、こういうデータ不整合を発見する場合以外にも、次のような商品分析のケースにも応用がききます。

問い:下の商品テーブルから、値段が同じ商品の組み合わせを取得せよ。
Products
商品名(name)値段(price)
りんご50
みかん100
ぶどう50
スイカ80
レモン30
いちご100
バナナ100
答え:さっきの住所録の例題と構造的にまったく同じです。
    家族ID → 値段
    住所 → 商品名

 に置き換えてください。すると、次のようになります。

--同じ値段だけど、商品名が違うレコードを検索するSQL
SELECT DISTINCT P1.name, P1.price
  FROM Products P1,
       Products P2
 WHERE P1.price = P2.price
   AND P1.name <> P2.name;
結果
name        price
------      ------
りんご         50
ぶどう         50
いちご        100
みかん        100
バナナ        100

 この場合は、住所録の例題と違ってDISTINCTをつけないと結果に冗長な行が現れるので注意してください。ポイントは、同一のキーを持つレコードの数です。住所録の場合も、もし前田家に子供がいれば、やはりDISTINCTがないと冗長な行が現れます。なお、結合の代わりに相関サブクエリを使って書けば、DISTINCTは不要になります。練習問題として、各自、書き換えてみてください。

ランキング

 ときどき、点数や人数、売上といった数値に基づく順位表を作るという案件に遭遇します。DBMSによっては、こういう場合に対応した独自拡張の機能を持っていることもあります(Oracle、DB2のRANK関数など)。しかし、実装依存の機能に頼るのは最後の選択肢です。まずは標準SQLの範囲内で実現することを考えましょう。

Products
商品名(name)値段(price)
りんご50
みかん100
ぶどう50
スイカ80
レモン30
バナナ50

 上のような商品テーブルから、値段の高い順に順位をつけます。ただし、同じ値段の商品は同じ順位になるようにして、次の順位は飛び石になるようにします。これは、次のように自己非等値結合(本当によく使う)を使って書きます。

--ランキング  1位から始まる。同順位が続いた後は不連続
SELECT P1.name,
       P1.price,
       (SELECT COUNT(P2.price)
          FROM Products P2
         WHERE P2.price > P1.price) + 1 AS rank
  FROM Products P1
 ORDER BY rank;
結果
name        price        rank
------      ------      ------
みかん        100           1
スイカ         80           2
りんご         50           3
ぶどう         50           3
バナナ         50           3
レモン         30           6

 おそらく、これが一般的な順位づけの方式だと思いますが、ここからカスタマイズもできます。スカラ・サブクエリの後ろの +1 を除外すれば、トップが0位から始まりますし、サブクエリ内の結合条件に等号を付けて「P2.price >= P1.price」とすることで、同じ値段の商品の順位を低い方に揃えることができます。また、「COUNT(DISTINCT P2.price)」とすることで、同じ順位のレコードが存在する場合でも、順位が飛び石にならず、連続的に出力されます(DENSE_RANK関数に相当)。このように、要件に応じてさまざまな順位づけ方式にカスタマイズすることが可能な、柔軟なSQLです。

 さて、それではこのSQLの動作について解説しますが、実はこれは、集合指向的な発想の格好の例題なのです。このサブクエリ内でやっていることは、自分よりも高い値段のレコード数を数えて、それを順位に使う、というものです。話を簡単にするために、値段から重複を除外して、

{ 100, 80, 50, 30 }

 という4つの値段で、トップを0位から始める場合について考えましょう。まず、一番高い100についてみると、これより高い値段は存在しませんから、COUNT関数は0を返します。次に、二番目に高い80の場合、自分より高い値段は、100の一つなので、COUNT関数は1を返します。以下同様に、50の場合は2を、30の場合は3を返します。すると、結果として、各値段について次のような集合を作っていることになります。

同心円的な再帰集合
集合値段自分より高い値段自分より高い値段の個数(これが順位になる)
S0100-0
S1801001
S250100, 802
S330100, 80, 503

 つまりこのSQLは、

S0 = φ
S1 = {100}
S2 = {100, 80}
S3 = {100, 80, 50}

 という「同心円的な」(セルコ)再帰的集合を作り、それらの要素数を数えているのです。「同心円的」という語が示すように、この4つの集合には

S3 ⊃ S2 ⊃ S1 ⊃ S0

 という包含関係が見て取れます。

集合の中に集合の中に集合の中に...
集合の中に集合の中に集合の中に...

 実は、「再帰的集合を用いた数の割り当て」という、このアイデア自体は目新しいものではありません。興味深いことに、集合論では100年以上前から使われている、自然数(0も含む)の再帰的定義(recursive definition)と同じものです。その方法も研究者によっていくつか流儀がありますが、今回の例題と同型なのは、コンピュータの父の一人である数学者フォン・ノイマンの考えた方法です。ノイマンは、0を空集合で定義することから始めて、順次、次のようなルールで自然数全体を定めました。

0 = φ
1 = {0}
2 = {0, 1}
3 = {0, 1, 2}
     ・
     ・
     ・

 0を定義したら、それを使って1を定義する。次に、0と1を使って2を定義する、次に、0と1と2を使って3を定義する、……以下同様。このやり方は、上のS0〜S3集合の作り方と構造的に同じものです(この比較をしたいがために、順位を0から始めるケースを例に使ったのでした)。SQLと集合論が直接的に結びついていることを示す好例と言えるでしょう。そして、両者をつなぐ道が、自己結合というわけです。

終わりに

 以上、4つの応用例を通して、自己結合について解説してきました。重要なポイントをまとめると、以下の4点になります。

  1. 非等値結合と組み合わせて使うのが基本
  2. GROUP BYと組み合わせると、再帰的集合を作ることができる
  3. 本当に異なるテーブルを結合していると考えると理解しやすい
  4. テーブルを行の集合に見立てて、集合指向的な発想で考えよう

 自己結合は、CASE式に劣らず強力な道具なので、ぜひ使いこなしてください。また非常に応用範囲の広い技術のため、本稿で紹介した使い方はほんの一端に過ぎません。まだまだ興味深い応用が多くあります。本稿で入門を終えたら、さらに先へと進んでください。

参考資料

←
1 2
 
INDEX
自己結合の使い方
はじめに
稼働環境
対象読者
重複順列・順列・組み合わせ
重複行を削除する
Page 2
部分的に不一致なキーの検索
ランキング
終わりに
参考資料
関連記事
db
db
評価を送信する


著者紹介
ミック (ミック)
主にOracleを使ったデータウェアハウス業務に従事するDBエンジニア。
HPのコンテンツ『リレーショナル・データベースの世界』。

著書:
達人に学ぶ SQL徹底指南書』(翔泳社、2008)

訳書:
ジョー・セルコ『SQLパズル 第2版』(翔泳社、2007)

講演資料:
「みんなまとめて面倒みよう」(デブサミ2008、2008/02/13)
コメント
(最新日付順:新着コメントRSS配信中!

■すみません…
コメント評:+1
 
Bad!
 
Good!

(前述で私の上司の名前リックと間違えて入力してしまいました)

ミックさま、大変お世話になったにも関わらず大変失礼しました。本当にありがとうございました。
■ありがとうございます!!
コメント評:+0
 
Bad!
 
Good!

リック様

さっそくご回答いただきましてありがとうございました。
初歩的な間違いで本当に恥ずかしい限りですが、おかげさまで下記の記述で求めていたデータを取り出すことができました。
本当にありがとうございます。

SQL> select B.id
from store_configuration B
left outer join store_config_reference A
on B.id = A.store_configuration_id
where A.store_configuration_id is null;

これを機に、もう一度結合について勉強させていただこうと思っていましたので、教えていただいたURL大変参考になります。(歯抜けチェックも必要でしたので、大変助かりました)

一度コンテンツすべてを勉強させていただいたうえで、またご質問させていただくことがあるかもしれませんが、どうぞよろしくお願い致します。
■無題
コメント評:+0
 
Bad!
 
Good!

elph様

確かに、外部結合で差集合を作ることができます。

ただ、WHERE条件の「= NULL」の指定が間違っていると思います。「IS NULL」ではないでしょうか。

私のサイトの「1-3.完全外部結合で積集合と差集合を作る」などもお役に立つと思います(下記アドレス参照)。

http://www.geocities.jp/mickindex/database/db_TaT.html#LocalLink-full

あるいは、EXCEPT演算子を持っているDBMSであれば、それを使うことでも簡単に書けます(OracleならMINUS)。
■重複しない値を取り出す方法
コメント評:+0
 
Bad!
 
Good!

ミックさま、初めてメールさせていただきます。
今、仕事でシンガポールに来ている者ですが、あるSQL文で悩んでいていろいろ検索しているうちにこのページにたどり着きました。
初歩的な質問で申し訳ないのですが、重複しない値を取り出す方法についてご質問させてください。

共通のidを持つべき二つのテーブルAとBがありますが、BにAにはないデータが数十件ほど存在します。その差異である数十件を取り出したいのですが、以前、left joinで結合して、where条件にnullを指定することでできた記憶があります。

でも、いろいろな組み合わせを試してみましたが、うまくいきません。
例)
select B.id
from store_configuration B
left outer join store_config_reference A
on B.id = A.store_configuration_id
where A.store_configuration_id =null;

select B.id
from store_configuration B
left outer join store_config_reference A
on B.id = A.store_configuration_id
where B.id =null;

根本的に何かが間違っているのだとは思いますが、現在出先にいるため過去資料が確認できず、詳しい方のお力をお借りした方がよいと思い質問させていただきました。

突然ぶしつけなご質問の仕方で恐縮ですが、どうぞよろしくお願い致します。


■中級への関門です
コメント評:+0
 
Bad!
 
Good!

ありがとうございます。そういっていただけると、書いた甲斐があります。

自己結合は強力な技術なのですが、動作が分かりにくいのであまり一般的に広まっていないのです。自己結合が使えるようになると中級者です。がんばってください。
■MySQL1年で・・・
コメント評:+0
 
Bad!
 
Good!

自己結合を初めて知りました(恥)。
目から鱗です。これから活用していきたいと思います。
名前:*
メールアドレス(名前にリンク):
URL(名前にリンク):
タイトル:
内容(テキストのみ1200文字まで、リンクタグ入力不可):*
アイコン:
なし

利用規約に同意して

トラックバック
この記事のトラックバックURL:
スパム対策で、トラックバックはデフォルトで非公開とし、編集部チェックを通した上で公開させて頂いております。重複した登録などにご注意ください。
基本に戻ろう: 自己結合 [でびび より]
参照元:Back to basics:self joins < Eddie Awad’s Blog — beginning of translation — この記事はOracle結合シリーズの中の一つです。 目次: 1.基本に戻ろう: クロス結合 2.基本に戻ろう: 内部結合 3... 続きを読む
Trackback (1)
記事は編集作業を経て公開されていますが、あくまで情報提供を第一の目的としたものであり、 内容には、不正確な記述、執筆者の予断や誤解に基づくもの、リンク切れ、環境要件が古いものが含まれていることがあります。 記事(翻訳記事を除く)の訂正に関しては、編集部の判断により随時対応することがありますが、各著作権者および(株)翔泳社はその内容の完全性を一切保障しません。 「投稿」の性質上、各著作権者は読者より訂正の依頼があったとしても対応できないこともあります。 記事内容の運用により派生した損害を含むあらゆる結果について、各著作権者および(株)翔泳社は一切の責任を持ちません。 各著作権者は記事内容に関するあらゆるサポートに付いてもその義務を放棄しています。あくまで記事は投稿され、編集を経て、公開された時点で完結したものであり、公開以降もサポートするかどうかは各者の任意事項となります。あらかじめご了承ください。
最新ニュース ≫一覧
最新記事 ≫一覧
一般投稿
Windows PowerShell 入門(3)−スクリプト編
VB.NET版O/Rマッピングツール「ObjectService」の使い方(継承設定)
Advanced/W-ZERO3 [es]でカメラアプリを作ろう
Windows PowerShell 入門(2)−基本操作編 2
VBAでファイルをダウンロードする
最近のコメント
はじめまして。よろしくお願いします。 記事内では、RuntimeExcep...(tahakoda:03/11)
折り返しがなくて見づらいですが、サンプルです。 CREATE TABLE ...(ミック:03/04)
現在、中央値の勉強をしており、このHPに行き着きました。大変勉強させていただきま...(kissy:03/03)
編集部ブログ
書籍検索でSQLインジェクション()
イケテルRails勉強会@九州大学に参加してみました()
サイトメンテナンスと新会員制度システムに関するお知らせ()
OpenIDへの対応に悩み中()
Joelのプレゼンはなぜおもしろいのか()
サイト統計
はてなブックマーク合計数
昨日までの登録メンバー数昨日までの総メンバー数
昨日の訪問者数昨日の訪問者数