XMLマスターポイントレッスン ~ プロフェッショナル(データベース)編 ~
第6回 FLWOR式によるデータのJOIN

データディレクトテクノロジーズ株式会社(現:日本プログレス株式会社)
山田敏彦 YAMADA, Toshihiko

今回はFLWOR式によるデータの「JOIN」について解説します。JOINはデータを効率良く処理するために複数の表を結合する際に行ないますが、XQueryにはJOINを生成するためのキーワードはありませんので、JOINを実現する記述法はさまざまです。今回はこれらの記述法の中からいくつかの方法を紹介します。みなさんも本稿で紹介する以外の記述法がないかを考えてみてください。

JOINとは

まず「JOIN」について簡単に説明しておきましょう。SQLで複数の表を結合して扱うことをJOINと言います。結合の仕方によって、大きく「INNER JOIN」と「OUTER JOIN」の2つに分けることができます。INNER JOINは、複数の表の指定されたカラムの値が等しい行を結合した表を返します(図1)。

INNER JOINの仕組み

表Aと表Bのnumberカラムの値が等しい行を結合した表がSELECT文によって生成されています。
OUTER JOINも指定されたカラムの値が等しい行を結合することに変わりはありませんが、結合する表のうちの1つを基準として扱い、その表に存在する行はJOINの条件を満たさなくても、すべて結合表に加えます。基準とする表の指定の仕方に応じて「LEFT OUTER JOIN」「RIGHT OUTER JOIN」「FULL OUTER JOIN」の3種類の方法があります。LEFT OUTER JOINを指定すると、FROM句で指定した表(左側にある表)が基準の表となるので、表aの行はnumberカラムが等しい行が表bになくても結合表に加えられます(図2)。

LEFT OUTER JOINの仕組み

RIGHT OUTER JOINは、逆にJOIN句で指定した表(右側にある表)が基準になるので、表bのすべての行が結合表に加えられます(図3)。

RIGHT OUTER JOINの仕組み

FULL OUTER JOINを指定すると、表a、表bのすべての行が結合表に加えられます(図4)。

FULL OUTER JOINの仕組み

ここでは、ごくごく簡単な例を挙げて説明しましたので、詳細についてはSQLの解説書などで確認してください。

XQueryによるJOINの実現

それでは、XQueryではどのようにJOINを実現するのかを具体的に説明していきましょう。図1~図4のデータと同じデータを使用しますので、結果を比較しながら読み進めてください。また、図1~図4で使用したサンプル表をXML化したXMLドキュメントをLIST1に示します。

LIST1:図1~図4のサンプル表をXML化したXMLドキュメント

<tableA>
 <row>
  <number>1</number>
  <data>5</data>
 </row>
 <row>
  <number>2</number>
  <data>10</data>
 </row>
 <row>
  <number>3</number>
  <data>15</data>
 </row>
 <row>
  <number>4</number>
  <data>20</data>
 </row>
</tableA>

<tableB>  <row>   <number>2</number>   <data>6</data>  </row>  <row>   <number>4</number>   <data>12</data>  </row>  <row>   <number>6</number>   <data>18</data>  </row>  <row>   <number>8</number>   <data>24</data>  </row> </tableB>

INNER JOIN

まず、INNER JOINの例を見てみましょう。FLWOR式とコンストラクタについては前回で詳細に説明しましたので、今回はポイントを絞り込んで説明します。自信のない人は前回の内容を復習しておいてください。
INNER JOINを行なうFLWOR式の例はLIST2のようになります。

LIST2:INNER JOINを行なうFLWOR式の例

<result>
 {
  for $a in fn:doc("TableA.xml")//row,
   $b in fn:doc("TableB.xml")//row
  where $a/number = $b/number
  return
   <row>
    <a_number>{ $a/number/text() }</a_number>
    <a_data>{ $a/data/text() }</a_data>
    <b_number>{ $b/number/text() }</b_number>
    <b_data>{ $b/data/text() }</b_data>
   </row>
 }
</result>

また、3〜4行目にあるfor句で、TableA.xmlとTableB.xmlの直積がタプルストリーム(Tuple Stream)になります(コラム「タプルストリームとは」を参照)。

for $a in fn:doc("TableA.xml")//row,
$b in fn:doc("TableB.xml")//row

このタプルストリームにwhere句の「$a/number = $b/number」という条件を作用させて絞り込みをかけているので、TableAのnumber要素とTableBのnumber要素が等しいタプルについてのみretun句が評価されます。実行結果は次のようになります。図1の結合表と比較してみてください。

<result>
 <row>
  <a_number>2</a_number>
  <a_data>10</a_data>
  <b_number>2</b_number>
  <b_data>6</b_data>
 </row>
 <row>
  <a_number>4</a_number>
  <a_data>20</a_data>
  <b_number>4</b_number>
  <b_data>12</b_data>
 </row>
</result>

なお、3〜4行目のfor句は次のように書くこともできます。どちらの書き方をしてもかまいません。

for $a in fn:doc("TableA.xml")//row
for $b in fn:doc("TableB.xml")//row

LEFT/RIGHT OUTER JOIN

LEFT/RIGHT OUTER JOINを行なうFLWOR式の例は、LIST3のようになります。

LIST3:LEFT/RIGHT OUTER JOINを行なうFLWOR式の例

<result>
 {
  for $a in fn:doc("TableA.xml")//row,
   $b in fn:doc("TableB.xml")//row
  where $a/number = $b/number
  return
   <row>
    <a_number>{ $a/number/text() }</a_number>
    <a_data>{ $a/data/text() }</a_data>
    <b_number>{ $b/number/text() }</b_number>
    <b_data>{ $b/data/text() }</b_data>
   </row>
   , 
  for $b in fn:doc("TableB.xml")//row
  where fn:empty(fn:doc("TableA.xml")//row[number =
  $b/number])
  return
   <row>
    <b_number>{ $b/number/text() }</b_number>
    <b_data>{ $b/data/text() }</b_data>
   </row>
 }
</result>

このクエリは、12行目の「」までのFLWOR式と、それ以降のFLWOR式の結果を13行目のカンマ演算子で結合しています。前半のFLWOR式は先ほどのINNER JOINの例と同じですから、次のように2つのrow要素からなるシーケンスが返されます。

<row>
 <a_number>2</a_number>
 <a_data>10</a_data>
 <b_number>2</b_number>
 <b_data>6</b_data>
</row>
<row>
 <a_number>4</a_number>
 <a_data>20</a_data>
 <b_number>4</b_number>
 <b_data>12</b_data>
</row>

後半のFLWOR式のポイントは、where句です。fn:empty()関数はパラメータで指定されたシーケンスが空シーケンスの場合はTrue、1つでも項目がある場合にはFalseを返します。この例で指定しているパラメータは$b/numberと等しいnumberを持つTableAのrow要素からなるシーケンスです。つまり、$b/numberと同じ値を持つTableA/row/numberが1つもない場合だけwhere句の条件が成立するので、前半のINNER JOINの対象にならなかったTableBのrow要素だけがreturn句で評価されます。

後半のFLWOR式の評価結果は、次のようなシーケンスになります。

<row>
 <b_number>6</b_number>
 <b_data>18</b_data>
</row>
<row>
 <b_number>8</b_number>
 <b_data>24</b_data>
</row>

前半のFLWOR式と後半のFLWOR式をそれぞれの評価結果と置き換えてみましょう(LIST4)。

LIST4:前/後半のFLWOR式をそれぞれの評価結果と置き換える

<row>
 <a_number>2</a_number>
 <a_data>10</a_data>
 <b_number>2</b_number>
 <b_data>6</b_data>
</row>
<row>
 <a_number>4</a_number>
 <a_data>20</a_data>
 <b_number>4</b_number>
 <b_data>12</b_data>
</row>
,
<row>
 <b_number>6</b_number>
 <b_data>18</b_data>
</row>
<row>
 <b_number>8</b_number>
 <b_data>24</b_data>
</row>

2つのシーケンスをカンマ演算子で結合すると1つのシーケンスになりますから、クエリの評価結果はLIST5のようになります。

LIST5:LIST4のクエリの評価結果

<result>
 <row>
  <a_number>2</a_number>
  <a_data>10</a_data>
  <b_number>2</b_number>
  <b_data>6</b_data>
 </row>
 <row>
  <a_number>4</a_number>
  <a_data>20</a_data>
  <b_number>4</b_number>
  <b_data>12</b_data>
 </row>
 <row>
  <b_number>6</b_number>
  <b_data>18</b_data>
 </row>
 <row>
  <b_number>8</b_number>
  <b_data>24</b_data>
 </row>
</result>

クエリ後半のFLWOR式のfor句、where句のTableAとTableBを入れ替えると、TableAの全データを含んだOUTER JOINを得ることができます。図2のLEFT OUTER JOIN、図3のRIGHT OUTER JOINの結合表と比較してみてください。
もし、TableB.xmlのnumber要素に重複する値がないことがあらかじめ分かっていれば、LIST6のような書き方も可能です。

LIST6:number要素に重複する値がないことがあらかじめ分かっている場合の記述例

<result>
 {
  for $a in fn:doc("TableA.xml")//row
  return
   <row>
    <a_number>{ $a/number/text() }</a_number>
    <a_data>{ $a/data/text() }</a_data>
    {
     for $b in fn:doc("TableB.xml")//row
     where $b/number = $a/number
     return 
     (<b_number>{ $b/number/text() }</b_number>,
      <b_data>{ $b/data/text() }</b_data>)
    }
   </row>
 }
</result>

[処理結果] <result>  <row>   <a_number>1</a_number>   <a_data>5</a_data>  </row>  <row>   <a_number>2</a_number>   <a_data>10</a_data>   <b_number>2</b_number>   <b_data>6</b_data>  </row>  <row>   <a_number>3</a_number>   <a_data>15</a_data>  </row>  <row>   <a_number>4</a_number>   <a_data>20</a_data>   <b_number>4</b_number>   <b_data>12</b_data>  </row> </result>

LIST6の書き方をした場合、TableBの複数のrow要素がTableAの単一のrow要素に対応すると、結合したXML文のフォーマットが崩れてしまうので注意してください。
例えば、TableBに次のようなrow要素を追加した場合、結果はLIST7のようになります(太字の部分)。

LIST7:TableBにrow要素を追加した場合の実行結果

<result>
 <row>
  <a_number>1</a_number>
  <a_data>5</a_data>
 </row>
 <row>
  <a_number>2</a_number>
  <a_data>10</a_data>
  <b_number>2</b_number>
  <b_data>99</b_data>
  <b_number>2</b_number>
  <b_data>6</b_data>
 </row>
 <row>
  <a_number>3</a_number>
  <a_data>15</a_data>
 </row>
 <row>
  <a_number>4</a_number>
  <a_data>20</a_data>
  <b_number>4</b_number>
  <b_data>12</b_data>
 </row>
</result>

これは、2つ目のrow要素のフォーマットが期待しているものと異なっています。
<row>
 <number>2</number>
 <data>99</data>
</row>

FULL OUTER JOIN

それでは、FULL OUTER JOINを行なうFLWOR式は、どのように表現できるのでしょうか。LIST8に例を示します。

LIST8:FULL OUTER JOINを行なうFLWOR式の例

<result>
 {
  for $a in fn:doc("TableA.xml")//row,
   $b in fn:doc("TableB.xml")//row
  where $a/number = $b/number
  return
   <row>
    <a_number>{ $a/number/text() }</a_number>
    <a_data>{ $a/data/text() }</a_data>
    <b_number>{ $b/number/text() }</b_number>
    <b_data>{ $b/data/text() }</b_data>
   </row>
   , 
  for $a in fn:doc("TableA.xml")//row
  where fn:empty(fn:doc("TableB.xml")//row[number = $a/number])
  return
   <row>
    <a_number>{ $a/number/text() }</a_number>
    <a_data>{ $a/data/text() }</a_data>
   </row>
   , 
  for $b in fn:doc("TableB.xml")//row
  where fn:empty(fn:doc("TableA.xml")//row[number = $b/number])
  return
   <row>
    <b_number>{ $b/number/text() }</b_number>
    <b_data>{ $b/data/text() }</b_data>
   </row>
 }
</result>

複雑に見えるかもしれませんが、これまでに説明したテクニックを組み合わせているだけです。このクエリでは、3つのFLWOR式をカンマ演算子で結合しています。
最初のFLWOR式はINNER JOINを返します。2番目のFLWOR式は TableBに対応する要素のないTableAのrow要素を返します。ここまでは先ほど説明したLEFT/RIGHT OUTER JOINと同じです。FULL OUTER JOINを行なうには、さらにTableAに対応する要素のないTableBのrow要素を追加してやれば良いわけです。3番目のFLWOR式がその役割を果たしています。処理結果はLIST9のようになります。図4と比較してみてください。


LIST9:LIST8のクエリの実行結果

<result>
 <row>
  <a_number>2</a_number>
  <a_data>10</a_data>
  <b_number>2</b_number>
  <b_data>6</b_data>
 </row>
 <row>
  <a_number>4</a_number>
  <a_data>20</a_data>
  <b_number>4</b_number>
  <b_data>12</b_data>
 </row>
 <row>
  <a_number>1</a_number>
  <a_data>5</a_data>
 </row>
 <row>
  <a_number>3</a_number>
  <a_data>15</a_data>
 </row>
 <row>
  <b_number>6</b_number>
  <b_data>18</b_data>
 </row>
 <row>
  <b_number>8</b_number>
  <b_data>24</b_data>
 </row>
</result>

今月の確認問題

ここまで、XQueryのFLWOR式によるJOINについて解説しました。ここで、今回解説した内容について、しっかりと理解できているかどうかを確認問題でチェックしてみましょう。

問題

次の[shop.xml]と[books.xml]に対してXQueryによる問い合わせを実行する際、[期待する結果]を得ることのできる正しいXQueryを1つ選択してください。
ただし、XQueryプロセッサは、fn:doc関数で[shop.xml]と[books.xml]を正常に読み込むことができるものとします。
また、実行結果のXML宣言の有無やインデントを考慮しません。

[ shop.xml ]

<SHOP>
 <book id="B001" stock="10"/>
 <book id="B003" stock="20"/>
</SHOP>

[ books.xml ]

<BOOKS>
 <book id="B001" title="XML Basic"/>
 <book id="B002" title="XMLDB"/>
 <book id="B003" title="XQuery"/>
</BOOKS>

[ 期待する結果 ]

<result>
 <book id="B001">
  <title>XML Basic</title>
  <stock>10</stock>
 </book>
 <book id="B003">
  <title>XQuery</title>
  <stock>20</stock>
 </book>
</result>
</BOOKS>
  1. <result>{
     let $books := fn:doc("books.xml")//book
     for $book in fn:doc("shop.xml")//book
      where $book/@id = $books/@id
      return
       <book id="{$book/@id}">
        <title>{fn:data($books/@title)}</title>
        <stock>{fn:data($book/@stock)}</stock>
       </book>
    }</result>
  2. <result>{
     let $books := fn:doc("books.xml")
     for $book in fn:doc("shop.xml")//book
      return
       <book id="{$book/@id}">
        <title>{$books//book[@id eq $book/@id]/
        @title}</title>
        <stock>{$book/@stock}</stock>
       </book>
    }</result>
  3. <result>{
     let $books := fn:doc("books.xml")
     for $book in fn:doc("shop.xml")//book
      let $id := $book/@id
      return
       <book>{
        attribute id {$id},
        element title {fn:string($books//
        book[@id eq $id]/@title)},
        element stock {fn:string($book/
        @stock)}
       }</book>
    }</result>

解説

Aは一見正しそうに見えますが、クエリの2行目で「let」を使用している点で誤りです。この「let」を「for」に変更(これに伴って「:=」を「in」に変更)すれば[期待する結果]を得ることができます。
[shop.xml]と[books.xml]を結合するXQueryとして、前述のようにfor句で2つのXML文書をループさせる方法のほか、Bのように、一方のXML文書だけをループさせ、そしてもう一方のXML文書からは必要なデータだけをreturn節で取得する、といった方法もあります。ちょうどBのようにfor節で[shop.xml]のbook要素をループさせ、return句のtitle要素内で[books.xml]のデータを取得する方法です(変数$booksには[books.xml]がバインドされています)。ただし、Bはtitleおよびstock要素内で属性ノードを取得しているため、[期待する結果]にはなりません。[期待する結果]を得るためには、取得した属性ノードをfn:dataもしくはfn:string関数の引数にする必要があります。
これらのことから、正解はCです。return句のコンストラクタの記述の仕方がBとは異なっていますが、[shop.xml]と[books.xml]を結合する考え方はBと同じで、またreturn句内で取得した属性ノードもfn:string関数の引数になっています。

* * *

今回は、FLWOR式によるデータのJOIN方法を説明しました。XQueryには、JOINを生成するためのキーワードがあるわけではありません。ですから、今回の記事で紹介しているクエリ例以外にもJOINを実現するクエリはいくらでも考えられますし、ここで紹介したクエリ例がベストであるとも限りません。データの特性、環境などを考慮して、最も効率の良い方法を選択することが大事です。
最後に今回紹介したJOINを挙げておきます。

  • INNER JOIN
  • LEFT/RIGHT OUTER JOIN
  • FULL OUTER JOIN
次回は、関数やモジュールを中心に、これまで書籍や雑誌でもあまり解説されることのなかった項目について解説しようと思います。お楽しみに!


山田敏彦 (やまだとしひこ)

慶應義塾大学理工学部卒、日立ソフト、サイベースなどを経て、2003年より現職。みなさん、もう夏休みの旅行には行かれたでしょうか。私はこの数年、屋久島で縄文杉を見たいと思いつつ、まだ果たせずにいます。来年こそは見に行けると良いのですが……。


<掲載> P.198-203 DB Magazine 2007 October

ページトップへ▲

XMLマスターポイントレッスン ~ プロフェッショナル(データベース)編 ~ 記事一覧へ戻る

HOMEへ戻る