目次


XQuery を使用した開発: データベース・プログラマーのための優れたプログラミング言語

迅速な開発が可能で保守が容易な XQuery のメリットを活用する

Comments

概要

SQL が作られた目的は、データを処理、操作、あるいは変換するためのコードのアルゴリズムをプログラマーが抽象化できるように、またアルゴリズムの実装がデータへのアクセスやデータ永続化の詳細に埋もれないようにするためです。この目標に関して、SQL は素晴らしい成功を収めました。SQL を使用することで、非常に複雑な OLTP アプリケーションやレポート・システム、データ・ウェアハウス、ビジネス分析システムを作成することができます。データベースの設計における SQL とリレーショナル・モデルは深く関係しており、SQL の強力さと柔軟さから、リレーショナル・モデルは過去何十年もの間、支配的なデータ・モデルでした。

この成功の影で失われたものが階層型データベースです。階層型データベースでは、さまざまなタイプのレコードが互いに親子関係を持つことができ、データは森構造になっていました。この方式のデータベースに使用されたプログラミング言語では、プログラマーはデータへのアクセスとデータ永続化のメカニズムを SQL ほどには抽象化することができなかったため、こうした制約によってこの言語は廃れていきました。

XML 文書は階層構造を使用してエンティティー間の関係を表現します。XQuery は 1 つまたは複数の文書の XML データを処理するために使用されます。また XQuery を使用すると、SQL とまったく同じようにデータ・アクセスとデータ永続化のメカニズムを抽象化できる一方、SQL よりも優れたプログラミング機能を実現することができます。XQuery の登場により、上位レベルでデータを処理するための技術が完全な形になったのです。XQuery にはプログラマーにとって魅力的で興味深い側面がいくつかあります。この記事では、このような XQuery を使用するメリットについて説明します。

階層型データを扱う

まず、SQL よりも XQuery の方が明らかにメリットがあるいくつかの状況について調べてみましょう。RDBMS では、データベース内の 2 つの別々のテーブルで親子関係が表現されることがよくあります。例えば、複数の購入品目を含む 1 つの注文書がデータベース内で purchase_order テーブルと items テーブルで表現されている場合があります (purchase_order テーブルは注文番号と顧客番号の両方を含み、items テーブルは注文番号と品目番号を含みます)。items テーブルは purchase_order テーブルに対する外部キーの関係を持っており、SQL は、この種の関係を極めて適切に処理することができます。図 1リスト 1 を見てください。

図 1. 注文書用に設計された、親子関係を持つテーブル
親 (purchase_order) テーブルと子 (items) テーブルを示した図
親 (purchase_order) テーブルと子 (items) テーブルを示した図
リスト 1. 指定された注文書内のすべての品目を検出するための SQL 文
select item_no, item_desc 
from purchase_order po, items i 
where ord_no = 'A12345' 
and po.ord_no = i.ord_no

SQL の弱点が見えてしまうのは、データ同士の関係がどこまでつながっているのか予測できない場合です。例えば、1 人の管理者が多くの従業員を抱えており、それらの各従業員も多くの従業員を抱えており、さらに・・・といった可能性がある場合です。この場合、これらのデータを別の親テーブルと子テーブルで表現するのは現実的ではありません。その理由の 1 つは、関係の階層が最も深い場合に対応するようにテーブルを設計しなければならないからです。つまり管理階層の深さが最大 6 レベルの場合には、6 つのテーブルを設計しなければなりません。このような設計は極めて出来の悪い設計です。さらにもっと悪い点としては、レベル x の管理者がレベル n のすべての従業員の情報にアクセスするには、レベル x からレベル n までテーブルの結合をしなければなりません。この方法では、使用するリソースが高くつくことになります。

こうした場合に採用される典型的なソリューションでは、1 つのテーブルを持ち、そのテーブルの行同士が図 2 のように親子関係を持つようにします。従業員 ID (emp_id)、管理者の従業員 ID (mgr_id)、従業員名 (emp_name) の各列によって、各行の従業員同士の関係が示されます。この例では、Jack Brown は 2 人の従業員 (John Silver と Ron McDonald) を管理しています。John Silver は Jon Carino を管理しています。残念ながら、このようなデータを XML 形式のデータとしてクエリーを実行しようとすると、そのためのクエリーは作成や保守が困難なものになります。リスト 2 のコードを見てください。このコードでは 1 人の管理者に直接または間接的に管理される従業員を検出するための SQL 文を作成します。

図 2. 従業員データ用に設計された 1 つのテーブルの行同士が関係を持つ様子
従業員データを 1 つのテーブルで管理する設計方式を示す図
従業員データを 1 つのテーブルで管理する設計方式を示す図
リスト 2. 1 人の管理者に直接または間接的に管理される従業員を検出するための SQL 文
with reporting_to(emp_id, emp_name) as 
( 
     select emp_id, emp_name 
     from employees 
     where emp_name = 'John Silver' 

     union all 

     select direct_reports.emp_id, direct_reports.emp_name 
     from employees direct_reports, reporting_to 
     where reporting_to.emp_id = direct_reports.mgr_id 
) 
select emp_id, emp_name 
from reporting_to

ご覧のように、このクエリーは再帰を使用しており、簡単には理解することができません。XML データ型と XQuery を使用すると、この問題をはるかに容易に解決することができます。その場合には、組織全体を 1 つの XML 文書で表現し、その文書をテーブルの 1 行に配置します。その実装を示しているのがリスト 3リスト 4 です。プログラムで作成、保守する場合、先ほどの行同士に親子関係を持たせるのと、XML 文書を 1 行に配置するのとで、どちらの方が容易か判断してみてください。

リスト 3. XML で表現した従業員データ
<?xml version="1.0"?> 
<org> 
     <employee id="0001"> 
          <name>Jack Brown</name> 
          <employee id="0002"> 
               <name>John Silver</name> 
               <employee id="0004"> 
                         <name>John Silver</name> 
               </employee> 
          </employee> 
          <employee id="0003"> 
               <name>Ron McDonald</name> 
          </employee> 
     </employee>
</org>
リスト 4. XML データ型を使用した場合に従業員リストを検出する
select emp.emp_id, emp.emp_name 
from employees, xmltable( 
     '$ORG/org//employee[name = "John Silver"]/descendant-or-self::employee' 
     columns 
          emp_id char(4) path '@id', 
          emp_name varchar(254) path 'name/text()' 
) emp

また、階層構造のデータは皆さんが思う以上に一般的であることにも注意してください。例えば先ほどの例で、注文書に他の注文書を含めることはできません。そのため表面的には、この例はとても階層構造には思えないかもしれません。しかし注文書は失敗する可能性があります。つまり購入対象の 20 品目のうち、2 つの品目が購入できない、または十分な数量を購入できないかもしれません。従って、既存の注文書は購入可能な品目が入手されて閉じられ、購入できなかった品目に対して新しい注文書が生成されます。そうした場合、新しい注文書には古い注文書へのリンクが含まれていた方が適切です。すると注文書のチェーンができ、それらの注文書同士の関係は上で説明した従業員データの場合と非常によく似たものになります。

階層構造ではないデータを扱う

リレーショナルの世界でモデル化されるデータの中で、階層構造ではないデータは大きな部分を占めています。データベースへのアクセス、またはデータベースへの書き込みに使用されるプログラミング言語では、こうしたデータ・エンティティーを極めて適切に処理できることが重要です。これから説明する例では、階層構造ではないデータを処理する上で XQuery が非常に優れていること、そしてプログラマーの観点から、XQuery の方が SQL よりも優れたソリューションであることを説明します。

ケース・スタディー 1: 最も近いものを検出し、それを使用する

この例では、複数のプログラミング・タスクがコンテスト (「イベント (EVENT)」) として構成されているケースを扱います。各イベントの作成者つまり管理者は、イベントに対して特定の「ポイント (POINTS)」を設定します。また、「価格ポイント (PAYMENT)」のルックアップ・テーブルが用意されており、各価格ポイントには「レベル (LEVEL)」という整数のラベルが指定されます。ここで処理される問題は、イベントの「ポイント (POINTS)」の値に最も近い価格ポイントを見つけ、その価格ポイントの値をそのイベントの「価格 (PRICE)」(コンテストの勝者に支払われる額) として使用するというものです。最も近いレベルが 2 つある場合には、高い方のレベルを使用して、適用される価格ポイントを決定します。

リスト 5リスト 6 は標準的な設計のデータベースとデータの例を示しています。

リスト 5. XML データ型ではないデータ型を使用したデータベース・テーブルの設計
[db2pe@lc4eb4168274532 code]$ db2 describe table pricing 
                                Data type                     Column 
Column name                     schema    Data type name      Length     Scale Nulls 
------------------------------- --------- ------------------- ---------- ----- ------ 
EVENT_ID                        SYSIBM    INTEGER                      4     0 No 
POINTS                          SYSIBM    INTEGER                      4     0 Yes 
PRICE                           SYSIBM    INTEGER                      4     0 Yes 

     3 record(s) selected. 

[db2pe@lc4eb4168274532 code]$ db2 describe table pricing_tier 
                                Data type                     Column 
Column name                     schema    Data type name      Length     Scale Nulls 
------------------------------- --------- ------------------- ---------- ----- ------ 
LEVEL                           SYSIBM    INTEGER                      4     0 No 
PAYMENT                         SYSIBM    INTEGER                      4     0 Yes 

     2 record(s) selected.
リスト 6. 上記のテーブルのデータの例
[db2pe@lc4eb4168274532 code]$ db2 "select * from pricing fetch first 5 rows only" 

EVENT_ID    POINTS      PRICE 
---------- ---------- ----------- 
      10472         640           0 
      10471         220           0 
      10470         190           0 
      10469         180           0 
      10466         780           0 

     5 record(s) selected. 

[db2pe@lc4eb4168274532 code]$ db2 "select * from pricing_tier" 

LEVEL       PAYMENT 
----------- ----------- 
          0          60 
          1         120 
          2         240 
          3         360 
          4         480 
          5         600 
          6         720 
          7         840 

     8 record(s) selected.

リスト 7 は必要な SQL update 文を示しています。

リスト 7. 標準的な SQL を使用して pricing テーブルを更新するための update 文
update pricing o 
    set (price) = ( 
        select payment 
        from ( 
            select pricing_info.event_id event_id , max(level) matched_level 
            -- "min_values" will have the smallest difference between the current 
            -- price and all the pre-defined price points for each event. "pricing_info" 
            -- will contain an index of all the price point differences tabulated 
            -- by the price point level. A join of these two tables by event_id 
            -- and price point difference, should get you the price point level that 
            -- you are seeking for each event. 
            from ( 
                select event_id, min(absdiff) 
                from ( 
                    -- For each event_id, calculate the absolute difference 
                    -- between the existing price and the price points 
                    select event_id, abs(points - payment) absdiff, level 
                    from pricing, pricing_tier 
                ) 
                group by event_id 
            ) as min_values(event_id, closest_match), ( 
                -- For each event_id, calculate the absolute difference 
                -- between the existing price and the price points 
                select event_id, abs(points - payment) absdiff, level 
                from pricing, pricing_tier 
            ) as pricing_info(event_id, absdiff, level)
            where min_values.event_id = pricing_info.event_id 
            and closest_match = absdiff 
            group by pricing_info.event_id 
        )x , pricing_tier y 
        where x.matched_level = y.level 
        and x.event_id = o.event_id 
    )

今度は XML データ型を使用した実装を見てください。pricing_tier テーブルとイベントの詳細をリスト 8リスト 9 の XML 文書で表現することができます。

リスト 8. XML 文書で表現された価格情報
<pricing> 
     <pricingtier level="0"><price>60</price></pricingtier> 
     <pricingtier level="1"><price>120</price></pricingtier> 
     <pricingtier level="2"><price>240</price></pricingtier> 
     <pricingtier level="3"><price>360</price></pricingtier> 
     <pricingtier level="4"><price>480</price></pricingtier> 
     <pricingtier level="5"><price>600</price></pricingtier> 
     <pricingtier level="6"><price>720</price></pricingtier> 
     <pricingtier level="7"><price>840</price></pricingtier> 
</pricing>
リスト 9. XML 文書で表現されたイベントの詳細
<event id="9083" eventstate="Cancelled: Client request"> 
     <title>UCD research 5</title> 
     <points>170</points> 
</event>

リスト 10 は使用されるテーブルの設計を示しています。

リスト 10. XML データ型を使用する場合のデータベース・テーブルの設計
[db2pe@lc4eb4168274532 code]$ db2 describe table events 
                                Data type                     Column 
Column name                     schema    Data type name      Length     Scale Nulls 
------------------------------- --------- ------------------- ---------- ----- ------ 
EVENT_ID                        SYSIBM    INTEGER                      4     0 No 
EVENT                           SYSIBM    XML                          0     0 No 

     2 record(s) selected. 

[db2pe@lc4eb4168274532 code]$ db2 describe table pricing 
                                Data type                     Column 
Column name                     schema    Data type name      Length     Scale Nulls 
------------------------------- --------- ------------------- ---------- ----- ------ 
PRICING                         SYSIBM    XML                          0     0 Yes 

     1 record(s) selected.

この設計では、価格情報が <points> 要素の兄弟要素として文書に挿入されると想定しています。リスト 11 は、それを実行するための update 文を示しています。

リスト 11. XML データ型と XQuery を使用した update 文
update events o 
   set (event) = ( 
      select xmlquery(' 
         (: 
          : First build a series of "pair" elements which map a pricing level number 
          : to a value that is the absolute value of the difference between the 
          : event points and the price point. Once you have this sequence, sort by 
          : the difference and pick the lowest value. This results in you picking the 
          : closest price match. Because you break ties for the closest value by picking 
          : the higher level, you add a second sort key (level) in descending order.
          :) 
         let $closestMatch := 
            ( 
               for $pair in ( for $p in $PRICING/pricing/pricingtier 
                  let $lp := xs:int($EVENT/event/points) 
                  let $absdiff := abs($lp - xs:int($p/price)) 
                  return 
                     <pair> 
                        <level>{$p/@level}</level> 
                        <diff>{$absdiff}</diff> 
                     </pair> 
               ) 
               order by xs:int($pair/diff/text()), 
                  xs:int($pair/level/text()) descending 
               return $pair 
            )[1] 
         return 
            transform 
               copy $e := $EVENT 
            modify 
               do insert 
               <pricing>{ 
                  $PRICING/pricing/pricingtier[@level = $closestMatch/level/@level]/ 
                     price/text() 
               }</pricing> after $e/event/points 
            return $e 
         ') 
         from events a, (select pricing from pricing fetch first row only) b 
         where a.event_id = o.event_id 
      )

ここで注目すべき主な点は、このプログラミング・スタイルが手続き型プログラミングと非常によく似ていることです。つまり中間的な計算が行われて結果が変数に割り当てられ、その変数が別の場所で再利用されています。さらに、手続き型プログラマーにとっては自然なものである、ネストされた for ループまであります。リスト 7 の SQL 文は、機能と手法は通常の手続き型プログラミングと同じですが、プログラミング・スタイルが大きく異なるため、理解しにくくなっています。XQuery を使用するスタイルは、プログラムを作成するのも保守するのも容易なため、プログラマーの生産性を高めることができます。

ケース・スタディー 2: 時系列データの中から関連するイベントを検出する

時系列データというのは、あるエンティティーの状態を、時間と共に変化する 1 つまたは複数の側面として表現したものです。このような状態の変化をデータとして保存する方法はたくさんあります。簡単のため、これから示す例では 1 つの変数の変化に注目し、データ・マイニングの世界で非常に一般的なデータ処理を見てみましょう。上位レベルで見ると、この処理では、ある特徴に従って状態が変化する一連のエンティティーを特定しようとします。そうしたエンティティーの例としては、スープを購入したすぐ後に洗剤を購入する顧客などが考えられます。このケース・スタディーでは先ほどの例を拡張し、2000年から 2009年の間に 2 階級以上昇進した従業員を検索します。リスト 12 を見てください。

リスト 12. 従業員の昇進履歴を保存するためのテーブル構造
[db2pe@lc4eb4168274532 code]$ db2 describe table pay_history 

                                Data type                     Column 
Column name                     schema    Data type name      Length     Scale Nulls 
------------------------------- --------- ------------------- ---------- ----- ------ 
EMP_ID                          SYSIBM    CHARACTER                    4     0 No 
START_TIME                      SYSIBM    TIMESTAMP                   10     6 No 
END_TIME                        SYSIBM    TIMESTAMP                   10     6 Yes 
JOB_LEVEL                       SYSIBM    INTEGER                      4     0 No 

     4 record(s) selected.

リスト 13 は、上記の目的のために必要な SQL 文を示します。

リスト 13. 指定された期間に 2 階級以上昇進した従業員を特定するための select 文
select emp_id, count(emp_id) 
from pay_history 
where start_time > '2000-01-01-00.00.00.000000' 
and end_time < '2010-01-01-00.00.00.000000' 
group by emp_id 
having count(emp_id) > 1

リスト 14 は XML データ型をベースに設計したテーブルを示しています。

リスト 14. XML データ型を使用して従業員の昇進履歴を保存するためのテーブル構造
[db2pe@lc4eb4168274532 code]$ db2 describe table pay_history 
                                Data type                     Column 
Column name                     schema    Data type name      Length     Scale Nulls 
------------------------------- --------- ------------------- ---------- ----- ------ 
EMP_ID                          SYSIBM    CHARACTER                    4     0 No 
HISTORY                         SYSIBM    XML                          0     0 No 

     2 record(s) selected.

リスト 15 は職務履歴を表す XML データを示しています。

リスト 15. 職務履歴を表す XML データ
<employee id="0001"> 
     <pay startDate="2001-11-23T00:00:00.000000Z" endDate="2002-10-07T00:00:00.000000Z" 
          level="4">70500</pay> 
     <pay startDate="2002-10-07T00:00:00.000000Z" endDate="2005-06-18T00:00:00.000000Z" 
          level="5">81500</pay> 
     <pay startDate="2005-06-18T00:00:00.000000Z" endDate="2007-06-01T00:00:00.000000Z" 
          level="6">96700</pay> 
     <pay startDate="2007-06-01T00:00:00.000000Z" level="7">120000</pay> 
</employee>

リスト 16 はリスト 13 の select 文と等価な select 文を XQuery を使用して表現しています。

リスト 16. 指定された期間に 2 階級以上昇進した従業員を特定するために XQuery を使用した select 文
select emp_id 
from pay_history 
where xmlexists(' 
     let $numPromotions := count( 
               $HISTORY/employee/pay[@startDate gt "2000-01-01T00:00:00.000000Z" 
                    and @endDate lt "2010-01-01T00:00:00.000000Z"] 
          ) 
     return 
          if($numPromotions gt 1)then 
          ( 
               true() 
          )else() 
')

XQuery を使用しても何も改善されていないように見えます。そこで、追加の要件を考えてみましょう。従業員は降格になる場合もあれば、昇進する場合もあります。職務履歴の各レコードは昇進と同時に降格も表現しているかもしれません。昇進のみに注目し、降格をすべて排除すると、XQuery を使用した select 文はリスト 17 のコードのように変わります。

リスト 17. 昇進のみに注目して職務履歴を処理する XQuery
select emp_id 
from pay_history 
where xmlexists(' 
   let $i := 0 
   (: 
    : You have to work around the fact that the DB2 pureXML implementation of XQuery 
    : does not support the preceding-sibling axis. To do this, iterate through the 
    : "pay" elements for a given employee, and tack on the position of the element. 
    : Later on, use this position to determine the previous "pay" element. 
    :) 
   let $pairs := ( 
      for $jobChanges in $HISTORY/employee/pay[@startDate gt 
         "2000-01-01T00:00:00.000000Z" and @endDate lt "2010-01-01T00:00:00.000000Z"] 
      let $i := $i + 1 
      return 
         <pair><position>{$i}</position>{$jobChanges}</pair> 
   ) 
   let $numPromotions := count( 
      for $p in $pairs 
      let $currentPos := xs:int($p/position) 
      return 
         (: If this is the first "pay" element, its not a demotion :) 
         if($currentPos eq 1)then 
         ( 
            $p/pay 
         )else( 
            if($pairs[$currentPos - 1]/data(@level) lt $p/data(@level))then 
            ( 
               $p/pay 
            )else() 
         ) 
   ) 
   return 
      if($numPromotions gt 1)then 
      ( 
         true() 
      )else() 
')

これと等価な SQL 文で XML データ型ではないデータ型を使用する場合、今度は自己結合を実装しなければなりません。自己結合を作成するとしても、その作業は極めて複雑で困難です。作業を少し楽にするための方法としては、「job history row number (職務履歴行番号)」列を pay_history テーブルに追加し、この列に対して自己結合できるようにする方法があります。この方法については読者の演習とすることにします。

まとめ

この記事の目的は、XQuery によってどんなことができるのかを列挙することではありません。この記事では例を紹介しながら、XQuery 言語に用意された強力な機能により、いかに短時間でアプリケーションを作成することができ、いかにアプリケーションの保守が容易になるかを説明しました。これらの例は、データベースに XML データ型を採用することを促す十分な動機になったのではないかと思います。


ダウンロード可能なリソース


関連トピック

  • Port CONNECT BY to DB2」(Serge Rielau 著、developerWorks、2005年10月): 階層型データを処理するための再帰クエリーの使用方法について、この優れた入門記事を読んでください。
  • DB2 9 での pureXML: XML データのクエリーに使う手段を使い分ける」(Matthias Nicola と Fatma Ozcan の共著、developerWorks、2007年): XML データ型を処理するために DB2 pureXML に用意されたさまざまな機能について非常に詳しく取り上げたチュートリアルで学んでください。
  • developerWorks の XML ゾーン: DTD、スキーマ、XSLT など、XML の領域でのスキルを磨くためのリソースが豊富に用意されています。XML 技術文書一覧に用意された、さまざまな技術記事やヒント、チュートリアル、技術標準、IBM Redbooks を見てください。
  • IBM XML certification: XML および関連技術において IBM 認定技術者になる方法を参照してください。
  • developerWorks on Twitter: 今すぐ Twitter に参加して developerWorks のツイートをフォローしてください。
  • developerWorks podcasts: ソフトウェア開発者のための興味深いインタビューや議論を聞いてください。
  • IBM 製品の評価版: IBM 製品の評価版をダウンロードするか、あるいは IBM SOA Sandbox のオンライン試用版で、DB2、Lotus、Rational、Tivoli、WebSphere などが提供するアプリケーション開発ツールやミドルウェア製品を試してみてください。

コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=XML, Open source
ArticleID=801299
ArticleTitle=XQuery を使用した開発: データベース・プログラマーのための優れたプログラミング言語
publish-date=03152012