【Oracle】XMLType完全ガイド|XML の格納・XMLQuery・XMLTABLE・XMLElement/XMLAgg まで解説

Oracle は XMLType という組み込みデータ型で XML データをネイティブに扱えます。外部システムから受け取った XML を DB に格納して XQuery でクエリしたり、既存テーブルのデータを XML 形式に変換して API レスポンスとして使ったりする場面で活躍します。

Oracle 12c 以降は JSON のサポートも強化されましたが、レガシーシステムの XML データ連携・EDI・SOAP Web サービスとの連携では XMLType が今も広く使われています。

この記事でわかること

  • XMLType 列を使って XML データをテーブルに格納する方法
  • XMLQuery / XMLExists で XQuery を使って XML を検索・抽出する方法
  • XMLTABLE で XML をリレーショナルデータ(行と列)に変換する方法
  • XMLElement / XMLForest / XMLAgg でテーブルデータを XML に変換する方法
  • 旧 API(EXTRACT / EXTRACTVALUE)と新 API(XMLQuery)の使い分け
  • XMLIndex で XML 検索を高速化する方法
スポンサーリンク

XMLType 列でテーブルに XML を格納する

XMLType 列を持つテーブルを作成して XML を挿入する
-- XMLType 列を持つテーブルを作成する
CREATE TABLE purchase_orders (
    order_id    NUMBER PRIMARY KEY,
    order_date  DATE,
    xml_data    XMLTYPE    -- XML データを格納する列
);

-- XML をリテラルとして INSERT する(XMLType() コンストラクタを使う)
INSERT INTO purchase_orders(order_id, order_date, xml_data)
VALUES(
    1,
    SYSDATE,
    XMLType('<?xml version="1.0"?>
<PurchaseOrder>
  <PoNumber>PO-20240401</PoNumber>
  <Vendor>ABC Corporation</Vendor>
  <Items>
    <Item>
      <ProductId>P001</ProductId>
      <Quantity>10</Quantity>
      <UnitPrice>500.00</UnitPrice>
    </Item>
    <Item>
      <ProductId>P002</ProductId>
      <Quantity>5</Quantity>
      <UnitPrice>1200.00</UnitPrice>
    </Item>
  </Items>
  <TotalAmount>11000.00</TotalAmount>
</PurchaseOrder>')
);

-- CLOB から XMLType に変換して挿入する
DECLARE
    v_xml_clob CLOB := '<Order><Id>2</Id><Status>PENDING</Status></Order>';
BEGIN
    INSERT INTO purchase_orders(order_id, order_date, xml_data)
    VALUES(2, SYSDATE, XMLType(v_xml_clob));
    COMMIT;
END;
/

-- XMLType を文字列(CLOB)に変換して取得する
SELECT order_id,
       xml_data.getClobVal() AS xml_text    -- XMLType → CLOB
FROM purchase_orders
WHERE order_id = 1;

XMLQuery で XQuery を使って XML を抽出・検索する

XMLQuery は XQuery 言語を使って XMLType の特定要素を抽出します。Oracle 10g 以降で推奨される XML 検索の標準的な方法です。

XMLQuery で特定要素を取得する
-- XMLQuery: XQuery 式で要素を抽出する
-- RETURNING CONTENT: XMLType として返す
-- PASSING: バインドする XMLType 列を指定する
SELECT
    order_id,
    XMLQuery(
        '$x/PurchaseOrder/PoNumber/text()'
        PASSING xml_data AS "x"
        RETURNING CONTENT
    ).getStringVal() AS po_number,

    XMLQuery(
        '$x/PurchaseOrder/TotalAmount/text()'
        PASSING xml_data AS "x"
        RETURNING CONTENT
    ).getNumberVal() AS total_amount

FROM purchase_orders
WHERE order_id = 1;
-- 出力: PO-20240401, 11000

-- XMLExists: 要素が存在するかどうかを検索条件にする
SELECT order_id
FROM purchase_orders
WHERE XMLExists(
    '$x/PurchaseOrder/Items/Item[ProductId="P001"]'
    PASSING xml_data AS "x"
);
-- P001 を含む注文を検索する

-- 複数の Item 要素を取得する(XMLTABLE と組み合わせる方が実用的)
SELECT
    order_id,
    XMLQuery(
        '$x/PurchaseOrder/Items'
        PASSING xml_data AS "x"
        RETURNING CONTENT
    ).getClobVal() AS items_xml
FROM purchase_orders
WHERE order_id = 1;

XMLTABLE で XML をリレーショナルデータに変換する

XMLTABLE は XML の繰り返し要素(例: <Item> タグが複数)をリレーショナルテーブルの行に変換します。XML 内の配列データを普通の SELECT で扱えるようになります。

XMLTABLE で XML の繰り返し要素を行に変換する
-- XMLTABLE: XML の Items/Item を1行ずつリレーショナルデータに変換する
SELECT
    p.order_id,
    x.product_id,
    x.quantity,
    x.unit_price,
    x.quantity * x.unit_price AS line_total
FROM
    purchase_orders p,
    XMLTABLE(
        '$doc/PurchaseOrder/Items/Item'   -- XQuery: 繰り返す要素のパス
        PASSING p.xml_data AS "doc"
        COLUMNS
            product_id  VARCHAR2(20)  PATH 'ProductId',    -- PATH で各列の XPath を指定
            quantity    NUMBER        PATH 'Quantity',
            unit_price  NUMBER        PATH 'UnitPrice'
    ) x
WHERE p.order_id = 1;

-- 出力例:
-- ORDER_ID  PRODUCT_ID  QUANTITY  UNIT_PRICE  LINE_TOTAL
--        1  P001              10      500.00     5000.00
--        1  P002               5     1200.00     6000.00

-- XMLTABLE と GROUP BY を組み合わせた集計クエリ
SELECT
    p.order_id,
    COUNT(x.product_id) AS item_count,
    SUM(x.quantity * x.unit_price) AS total_amount
FROM
    purchase_orders p,
    XMLTABLE(
        '$doc/PurchaseOrder/Items/Item'
        PASSING p.xml_data AS "doc"
        COLUMNS
            product_id  VARCHAR2(20)  PATH 'ProductId',
            quantity    NUMBER        PATH 'Quantity',
            unit_price  NUMBER        PATH 'UnitPrice'
    ) x
GROUP BY p.order_id;

XMLElement・XMLForest・XMLAgg でデータを XML に変換する

既存のリレーショナルテーブルのデータを XML 形式に変換するには、XMLElement(要素の生成)・XMLForest(複数要素をまとめて生成)・XMLAgg(複数行を XML に集約)を使います。

XMLElement・XMLForest・XMLAgg でデータを XML に変換する
-- XMLElement: 単一の XML 要素を生成する
SELECT
    XMLElement("Employee",
        XMLElement("EmployeeId", employee_id),
        XMLElement("Name", first_name || ' ' || last_name),
        XMLElement("Salary", salary)
    ).getClobVal() AS emp_xml
FROM employees
WHERE department_id = 50
FETCH FIRST 3 ROWS ONLY;

-- XMLForest: 複数の要素を一度に生成する(列名が要素名になる)
SELECT
    XMLElement("Employee",
        XMLForest(
            employee_id AS "EmployeeId",
            first_name  AS "FirstName",
            last_name   AS "LastName",
            salary      AS "Salary"
        )
    ).getClobVal() AS emp_xml
FROM employees
WHERE department_id = 50
FETCH FIRST 3 ROWS ONLY;

-- XMLAgg: 複数行の XML 要素を集約して 1 つの XML にまとめる
SELECT
    XMLElement("Department",
        XMLAttributes(department_id AS "id"),    -- 属性の追加
        XMLAgg(
            XMLElement("Employee",
                XMLForest(
                    employee_id AS "Id",
                    last_name   AS "Name",
                    salary      AS "Salary"
                )
            )
            ORDER BY last_name    -- ORDER BY で要素の順序を指定
        )
    ).getClobVal() AS dept_xml
FROM employees
GROUP BY department_id
HAVING department_id = 50;

-- 出力例(一部):
-- <Department id="50">
--   <Employee><Id>100</Id><Name>King</Name><Salary>24000</Salary></Employee>
--   <Employee><Id>101</Id><Name>Kochhar</Name><Salary>17000</Salary></Employee>
-- </Department>

旧 API(EXTRACT / EXTRACTVALUE)と新 API の使い分け

EXTRACT / EXTRACTVALUE は非推奨
EXTRACT(xmltype, xpath)EXTRACTVALUE(xmltype, xpath) はXPath 1.0 ベースの旧 API です。Oracle 12c 以降では非推奨とされており、将来のバージョンで削除される可能性があります。新規開発では XMLQuery / XMLExists / XMLTABLE を使ってください。
旧 API と新 API の対応
-- 旧 API (EXTRACTVALUE): 単一テキストノードを取得する(非推奨)
SELECT
    EXTRACTVALUE(xml_data, '/PurchaseOrder/PoNumber') AS po_number
FROM purchase_orders
WHERE order_id = 1;

-- 新 API (XMLQuery): 同等の処理(推奨)
SELECT
    XMLQuery(
        '$x/PurchaseOrder/PoNumber/text()'
        PASSING xml_data AS "x"
        RETURNING CONTENT
    ).getStringVal() AS po_number
FROM purchase_orders
WHERE order_id = 1;

-- 旧 API (EXTRACT): XMLType として取得する(非推奨)
SELECT
    EXTRACT(xml_data, '/PurchaseOrder/Items') AS items_xml
FROM purchase_orders
WHERE order_id = 1;

-- 新 API (XMLQuery RETURNING CONTENT): 同等の処理(推奨)
SELECT
    XMLQuery(
        '$x/PurchaseOrder/Items'
        PASSING xml_data AS "x"
        RETURNING CONTENT
    ).getClobVal() AS items_xml
FROM purchase_orders
WHERE order_id = 1;

XMLIndex で XML 検索を高速化する

XMLType 列に対して XMLExistsXMLQuery でフィルタリングするクエリが遅い場合、XMLIndex を作成することでパフォーマンスを改善できます。

XMLIndex の作成と確認
-- XMLIndex を作成する(XMLExists / XMLQuery の検索を高速化する)
CREATE INDEX po_xml_idx ON purchase_orders(xml_data)
INDEXTYPE IS XDB.XMLIndex
PARAMETERS ('PATHS (/PurchaseOrder/PoNumber, /PurchaseOrder/Items/Item/ProductId)');
-- PATHS: インデックス化する XPath を指定する(省略すると全パスを対象にする)

-- XMLIndex の確認
SELECT index_name, index_type, table_name, status
FROM USER_INDEXES
WHERE table_name = 'PURCHASE_ORDERS';

-- XMLIndex が使われることを確認する(実行計画)
EXPLAIN PLAN FOR
SELECT order_id
FROM purchase_orders
WHERE XMLExists(
    '$x/PurchaseOrder/Items/Item[ProductId="P001"]'
    PASSING xml_data AS "x"
);
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY(format => 'TYPICAL'));
-- XML INDEX SCAN が表示されれば XMLIndex が使われている

-- XMLIndex を削除する
DROP INDEX po_xml_idx;

まとめ

  • XMLType:Oracle 組み込みの XML データ型。XMLType() コンストラクタで文字列・CLOB から変換する。getClobVal() / getStringVal() / getNumberVal() で値を取り出す
  • XMLQuery:XQuery 式で XML 要素を抽出する。PASSING でバインド変数を指定。RETURNING CONTENT で結果を XMLType として受け取る
  • XMLExists:XQuery 条件で XML を検索する WHERE 句フィルタ。XMLIndex と組み合わせると高速
  • XMLTABLE:繰り返し要素(配列的な構造)をリレーショナル行に変換する。COLUMNS でXPathと型を指定
  • XMLElement / XMLForest / XMLAgg:リレーショナルデータを XML に変換する。XMLAttributes で属性を追加できる
  • EXTRACT / EXTRACTVALUE は非推奨:Oracle 12c 以降は XMLQuery / XMLTABLE を使う
  • XMLIndex:XMLExists / XMLQuery の検索対象パスを指定してインデックス化する

Oracle の JSON 処理(JSON_VALUE / JSON_TABLE)との比較は Oracle JSON完全ガイドを参照してください。UTL_HTTP で外部 XML API を呼び出してパースする方法は UTL_HTTP完全ガイドも参照してください。