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

★★★★★

自己結合の使い方

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

はじめに

 SQLが提供する結合演算には、その特徴に応じて内部結合、外部結合、クロス結合などさまざまな名前が与えられています。普通、これらの結合の多くは、異なるテーブルまたはビューを対象として行われます。しかし、SQLは結合が同一のテーブルまたはビューに適用されることを禁止していません。同一のテーブルを対象に行う結合を「自己結合(self join)」と呼びます。自己結合は、使いこなせば非常に便利な技術ですが、動作がイメージしにくいため敬遠されがちです。そこで本稿では、この自己結合の便利さを例題を通して学び、その動作を分かりやすく解説します。

 自己結合を理解することは、実務上のテクニックを身につける以外に、もう一つ利点があります。それは、集合指向(set-oriented)というSQLの重要な特徴を理解できることです。オブジェクト指向言語が世界をオブジェクトとして表現するように、SQLは世界を集合として表現します。自己結合は、SQLのこの特徴をうまく利用した技術です。きっと、本稿を読み終わるころには、今まで二次元表に見えていたテーブルが、少し集合らしく見えてくるはずです。

稼働環境

  • Oracle
  • SQL Server
  • DB2
  • PostgreSQL
  • MySQL(サブクエリを使うものについてはバージョン4.1以上)

対象読者

 GROUP BYEXISTS述語、通常の等結合、相関サブクエリ、スカラ・サブクエリについての知識があることが望ましいです。

重複順列・順列・組み合わせ

 次のような商品テーブルに、「りんご、みかん、バナナ」の3レコードが登録されているとします。売上を調べる統計表を作成する場合などに、これらの品物の組み合わせを取得したいことがあります。

Products
商品名(name)値段(price)
りんご100
みかん50
バナナ80

 「組み合わせ」とひとことで言っても、その種類は2つあります。一つが、並び順を意識した順序対(ordered pair)、もう一つが順序を意識しない非順序対(unordered pair)です。順序対は、<1, 2>のように尖った括弧で、非順序対は{1, 2}のような括弧で表記します。順序対は、順序が違えば別物なので、<1, 2> ≠ <2, 1>ですが、非順序対の場合は順序を無視するので、{1, 2} = {2, 1}です。

 さて、次のように単純に直積を作ると、順序対が得られます。

--重複順列を得るSQL
SELECT P1.name AS name_1, P2.name AS name_2
  FROM Products P1,
       Products P2;
結果
name_1      name_2
------      ------
りんご      りんご
りんご      みかん
りんご      バナナ
みかん      りんご
みかん      みかん
みかん      バナナ
バナナ      りんご
バナナ      みかん
バナナ      バナナ

 一行が一つの順序対を表します。結果行数は重複順列で 32 = 9 です。この結果には冗長な(りんご, りんご)という行が含まれますし、(りんご, みかん)と(みかん, りんご)という順序を変えただけの組み合わせも異なる行として現れます。これは、先に述べたように順序を意識した集合だからです。

 ここから、冗長な集合を排除する変更を考えます。まず、(りんご, りんご)のような同一要素の組み合わせを除外するには、次のように条件を追加した結合を行います。

--順列を得るSQL
SELECT P1.name AS name_1, P2.name AS name_2
  FROM Products P1,
       Products P2
 WHERE P1.name <> P2.name;
結果
name_1      name_2
------      ------
りんご      みかん
りんご      バナナ
みかん      りんご
みかん      バナナ
バナナ      りんご
バナナ      みかん

 「WHERE P1.name <> P2.name」という結合条件によって、同一要素の組み合わせを排除しています。結果行数の計算は順列で 3P2 = 6 です。この結合を理解するポイントは、次のような2つのテーブルが本当に2つあるのだと想像することです。

(りんご、りんご)の組み合わせはダメ
(りんご、りんご)の組み合わせはダメ

 もちろん、P1もP2も、物理的には同じ「Products」テーブルとして格納されています。しかし、SQLにおいて異なる別名が与えられたなら、たとえ同一のテーブルであっても、それらは異なるテーブル(集合)と見なされます。P1とP2はたまたま保持するデータが等しかっただけの、異なる2つの集合として考えられる、ということです。すると、この自己結合の動作は

  • P1の「りんご」行の結合対象は、P2の「みかん、バナナ」の2行
  • P1の「みかん」行の結合対象は、P2の「りんご、バナナ」の2行
  • P1の「バナナ」行の結合対象は、P2の「りんご、みかん」の2行

 というように、異なるテーブルを使う通常の結合と同様に考えることができます。このように考えれば、自己結合の「自己」という接頭辞にも大きな意味はありません。

 さて、この結果も、まだ順序対です。ここからさらに、(りんご, みかん)と(みかん, りんご)のような順序を入れ替えた組み合わせを排除することを考えます。次のSQLを見てください。

--組み合わせを得るSQL
SELECT P1.name AS name_1, P2.name AS name_2
  FROM Products P1,
       Products P2
 WHERE P1.name > P2.name;
結果
name_1      name_2
------      ------
りんご      みかん
りんご      バナナ
みかん      バナナ

 ここでもやはり、P1、P2という2つのテーブルが存在すると考えてください。結果行数の計算は組み合わせで 3C2 = 3 です。ここまで絞ってようやく非順序対が得られました。恐らく、私たちが普段「組み合わせ」と言うとき、念頭においているのはこのタイプのものでしょう。

 3つ以上の列を使いたいときも、次のように簡単に拡張できます。

--組み合わせを得るSQL  3列の拡張版
SELECT P1.name AS name_1, P2.name AS name_2, P3.name AS name_3
  FROM Products P1,
       Products P2,
       Products P3
 WHERE P1.name > P2.name
   AND P2.name > P3.name;

 この例題のように等号「=」以外の比較演算子である < や >、<> を使って行う結合を「非等値結合」と言います。それを自己結合と組み合わせているので、「自己非等値結合」です。列の組み合わせを作りたいときに多用するので、覚えておくとよいでしょう。

 また、>、< などの比較演算子は数値型の列に限らず、文字型でも辞書順比較として機能するということも、今回のちょっとしたワンポイントです。

重複行を削除する

 重複行というのは、リレーショナル・データベースの世界においてNULLと並んで嫌われる存在です。そのため、これを排除するための方法も数多く考えられています。例えば、先の例題で使った商品テーブルで、「みかん」に重複が生じているテーブルを考えます。このテーブルには恐ろしいことに主キーすら設定されていません(というより、設定できません)。こんなテーブルはすぐにでも「掃除」する必要があります。

重複行の削除
重複行の削除

 今回は、自己相関サブクエリを使って重複を削除する方法を紹介します。結合と相関サブクエリは演算としては異なりますが、考え方が似ていて、SQLを同値変換できる場合も多いので、本稿で一緒に扱います。

 重複行は2行でなくとも、何行あってもかまいません。一般に、重複する列が主キーを含まない場合は、主キーを使うことができますが、この例題のように全列について重複する場合は、実装依存のレコードIDを使う必要があります。レコードIDは「どんなテーブルでも使える主キー」という特徴を持つ擬似列だと考えてください。ここではOracleのrowidを使います*1

--重複行を削除するSQL  その1:極値関数の利用
DELETE FROM Products P1
 WHERE rowid < ( SELECT MAX(P2.rowid)
                   FROM Products P2
                  WHERE P1.name = P2. name
                    AND P1.price = P2.price ) ;

 これは、一見しただけでは、動作の分かりづらい相関サブクエリです。そもそも、2つのテーブル間の関連を記述するから「相関」サブクエリという名前なのに、1つのテーブルについて相関というのも、奇妙な表現です。

 この疑問が生じるのは、SQL を見るレベルを間違えているからです。この相関サブクエリも、先の例題と同様、実は次のような瓜二つの集合の関連を記述していると考えてください。

P1
レコードID(rowid)商品名(name)値段(price)
1りんご50
2みかん100
3みかん100
4みかん100
5バナナ80
P2
レコードID(rowid)商品名(name)値段(price)
1りんご50
2みかん100
3みかん100
4みかん100
5バナナ80

 ポイントは同じで、SQLの中で異なる名前の与えられた集合を、本当に別物として考えることです。このサブクエリは、P1とP2を比較して、名前と値段が等しいレコード集合のうち、その最大のrowidのレコードを返します。

 すると、重複が存在しないりんごとバナナの場合は、「1:りんご」と「5:バナナ」がそのまま返り、条件が不等号なので一行も削除されません。みかんの場合は「4:みかん」が返り、それより小さなrowidを持つレコード「2:みかん」と「3:みかん」の2行が削除されます。

 もうお分かりのように、SQLを実表のレベルで見るというのは、抽象度の低い見方です。「テーブル」「ビュー」というのは記憶方法に応じてつけられた名前ですが、SQLの動作を考える際には、データの記憶方法は(パフォーマンスを除けば)考慮する必要はありません。

 ところで、先の例題でも登場した非等値結合を使うことで、同じ動作をするSQLを書くことができます。どういう動作をしているのか、集合P1とP2を紙に書いて、確かめてみてください。

--重複行を削除するSQL  その2:非等値結合の利用
DELETE FROM Products P1
 WHERE EXISTS ( SELECT *
                  FROM Products P2
                 WHERE P1.name = P2.name
                   AND P1.price = P2.price
                   AND P1.rowid < P2.rowid );
*1
 こういうユーザが使用できるレコードIDを実装しているのは、Oracle(rowid)とPostgreSQL(oid)のみです。他のDBMSの場合は、必ず主キーを設定して、それを使うか、または別の方法(重複を排除した結果を別テーブルに挿入するなど)を使う必要があります。
 
1 2
→
INDEX
自己結合の使い方
Page 1
はじめに
稼働環境
対象読者
重複順列・順列・組み合わせ
重複行を削除する
部分的に不一致なキーの検索
ランキング
終わりに
参考資料
関連記事
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のプレゼンはなぜおもしろいのか()
サイト統計
はてなブックマーク合計数
昨日までの登録メンバー数昨日までの総メンバー数
昨日の訪問者数昨日の訪問者数