4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Oracle × Snowflake】Data Transformsで ADB の表を AICAT 管理の Iceberg テーブルに全量・差分ロードする

4
Posted at

Oracle Database の表を Apache Iceberg のテーブルとして書き出すとき、SQL で一括に書き出すほかに、画面で表を選んで定期的に送りたい場面があります。

ADB(Autonomous AI Database)に同梱の Oracle Data Transforms には、ロード先として Apache Iceberg を選べる接続があり、Oracle AI Data Catalog が管理するテーブルへ全量ロードと差分ロードができます。

この記事では、ADB の表を Data Transforms で Iceberg テーブルにロードするために、接続の作成からデータ・ロードの実行、そして差分ロードで Iceberg のスナップショットとファイルがどう増えるかまでを、実際に動かして確かめた手順で紹介します。

Oracle AI Data Catalog の有効化、ストレージの登録、SQL での書き出し、Snowflake からの参照の手順は、前の記事に書いていますので、詳しくはこちらをご覧ください。

前提条件

検証環境と使ったデータです。
確認日は 2026 年 10 月時点です。

項目 内容
Oracle Database Autonomous AI Lakehouse(Serverless、26ai、大阪リージョン)。Oracle AI Data Catalog(以下 AICAT)を有効化し、Object Storage を登録済み
Data Transforms ADB に同梱のもの。Database Actions から起動する
ストレージ OCI Object Storage のバケット。前の記事の手順 5 で AICAT にストレージとして登録済み。Data Transforms の接続には Access Key と Secret Key(顧客秘密キー)だけを渡し、エンドポイントの指定はない
資格証明 AICAT に接続する DB ユーザー(AICAT_USER ロール)と、Object Storage の顧客秘密キー
サンプルデータ CUSTOMERS(200 行)、ORDERS(1,000 行)、ORDER_ITEMS(3,000 行)。差分マージ用に、CUSTOMERS に UPDATED_AT 列を足した CUSTOMERS_M(200 行)

用語解説を 2 つだけ先に書いておきます。

  • データ・ロード は、Data Transforms でソースの表を選び、ロード先へ表の単位でコピーする機能です。
  • Iceberg REST カタログ は、Iceberg テーブルの名前と最新のメタデータの場所を REST API で管理する仕組みで、AICAT はこの API を提供します。

構成

Data Transforms がローカル表(ADB の中にある Oracle Database の表)を読み、Parquet とメタデータのファイルを Object Storage に書き、テーブルの作成とスナップショットのコミットを AICAT に依頼します。
書き出したテーブルは、ADB の SQL からも、Snowflake のような別のエンジンからも、AICAT を経由して読めます。
Snowflake 側の外部ボリュームとカタログ統合は、前の記事で作ったものをそのまま使います。

image.png

手順

それでは実際に進めていきましょう。
1〜3 が接続の準備、4〜6 が全量ロードと確認、7〜9 が差分ロード、10 が Snowflake 側の確認です。

1. Data Transforms を起動する

Database Actions から Data Transforms(画面の表記は「データ変換」)を開き、ADB のユーザー(ここでは ADMIN)でサインインします。

初回は「Provisioning Oracle Data Transforms」の画面が続き、10 分ほどでホームが表示されました。

dt-provisioning.jpg

ホームには「データのロード」「データの変換」「エクスポート」「インポート」のカードがあります。

c09-dt-home.jpg

左の「接続」を開くと、ホストの ADB への Oracle 接続(ここでは icebergsnowflake)が用意されているのが確認できます。
この接続をソースに使います。

c10-dt-connections-initial.jpg

2. Apache Iceberg 接続を作る

ロード先になる Apache Iceberg 接続を作ります。
「接続の作成」でタイプの検索に「Iceberg」と入れると、データベースのタブに「Apache Iceberg」が出ます。

dt-create-connection-type-iceberg.jpg

接続の詳細では、まず Iceberg カタログの Catalog Provider を選びます。

選択肢は

  • Generic Rest Catalog
  • Snowflake Open Catalog
  • Databricks Unity Catalog
  • Oracle AI Data Catalog の 4 つです。

c13-dt-catalog-provider-options.jpg

Oracle AI Data Catalog を選ぶと、
入力項目は

  • Oracle AI Data Catalog URL
  • ユーザー
  • データベース・パスワード
    の 3 つになります。

URL は、Database Actions と同じホスト名に /catalog を付けたもので、前の記事で Snowflake のカタログ統合に指定した値と同じです。

ユーザーは AICAT に接続できる DB ユーザーです。
(ドキュメントの手順にはカタログ名の入力がありますが、2026 年 10 月時点の画面では、Oracle AI Data Catalog を選ぶとカタログ名の欄は表示されませんでした。)

Storage Settings を開くと、Storage Provider は OCI Object Storage S3 Compatibility が既定で、Access Key と Secret Key を入れます。

Storage Settings には、カタログが資格証明ベンディングに対応しているときは無視される旨の注記があります。資格証明ベンディングは、カタログがストレージの一時的な資格証明を読み手に配る仕組みです。
現時点の AICAT はこれに対応しておらず(/v1/config の応答で無効になっています。前の記事の手順 11)、ここで顧客秘密キーを渡します。

c15-dt-aicat-form-filled.jpg

入力したら「接続のテスト」で確かめてから「作成」します。

c16-dt-aicat-connection-test-ok.png

3. ソース接続に資格証明を設定する

データ・ロードを作る前に、自動で用意された ADB の接続にユーザーとパスワードを設定します。
ドキュメントでも、プロビジョニング時に作られる既定の接続はユーザーとパスワードだけ編集できるとあります。
この接続はユーザーが未設定のまま用意されていて、そのままデータ・ロードのエディタを開くと「ソース接続 : ORA-01017: invalid credential or not authorized; logon denied」になります。

c21-dt-dataload-editor-ora01017.jpg

接続一覧から icebergsnowflake を開き、ユーザーに ADMIN、パスワードを入れて「更新」します。

注意点: 「接続のテスト」は入力中の値で検証します。テストのあとに「OK」でダイアログを閉じるとパスワード欄が伏せ字に戻り、その後の「更新」では保存されませんでした(2 回再現)。
パスワードを入力したら、テストを挟まずに「更新」を押します。

c23-dt-update-connection-adb-test-ok.png

4. データ・ロードを作って ORDERS を全量ロードする

ORDERS の 1,000 行を Iceberg テーブルとして全量ロードします。
ホームの「データのロード」を開き、3 つのステップで設定します。

  • ステップ 1 —
    名前(LOAD_ORDERS_TO_AICAT)と、新しいプロジェクト名(AICAT_DEMO)。「ロード処理中」は内部、「デプロイメント・タイプ」はデータ変換(バッチ)の既定のまま
  • ステップ 2 —
    ソース接続。接続タイプ Oracle、接続 icebergsnowflake、スキーマ ADMIN
  • ステップ 3 —
    ターゲット接続。接続タイプ Apache Iceberg、接続 AICAT_ICEBERG。ネームスペースは「既存のネームスペースから選択」で SALES

dt-dataload-step1.jpg

ネームスペースは Iceberg の名前空間で、SQL で書き出したときに作った SALES がそのまま一覧に出ます。

c20-dt-dataload-step3-target-namespace.jpg

保存するとデータ・ロード・エディタが開き、ソースの表が一覧になります。

ORDERS を選び、Target Action を「追加」にして保存します。
Target Action の選択肢は「増分マージ」「増分追加」「追加」「ロードしない」の 4 つで、Iceberg をロード先にしたときは Truncate と Recreate は出ません。
ドキュメントでも、Iceberg ターゲットの選択肢はこの 4 つです。

c25-dt-dataload-orders-append.jpg

「Run」を押して「開始」を選ぶと、ジョブが作られます。
データ・ロード・ステータスの「現在のステータス」が「完了」になるまで 1 分ほどでした。

c27-dt-dataload-completed.jpg

ジョブの詳細を開くと、行数 1000、挿入数 1000、エラー数 0 で、ジョブ全体は 75 秒、ORDERS のステップは 16 秒でした(1 回の実測)。

c30-dt-job-details.jpg

dt-jobs-list.jpg

5. 書き出された Iceberg テーブルを確認する

ロードで何が書かれたかを、バケットとメタデータから見ていきます。
バケットの SALES/ORDERS/ には、data/ の下に Parquet が 1 つ、metadata/ の下に metadata.json が 3 世代(00000〜00002)、マニフェストが 1 つ、マニフェストリストが 2 つできていました。

データファイルのパスは data/1001/0011/1110/10000001/…parquet のように、ハッシュのディレクトリが挟まっています。
表プロパティに write.object-storage.enabled = true が設定されているためで、SQL で書き出したときの data/ 直下とは配置が違います。

metadata.json から読み取れることをまとめます。

項目 値
format-version 2
スナップショット 1 つ。operation = append、added-records 1000、added-data-files 1
表プロパティ write.object-storage.enabled = true、write.parquet.compression-codec = zstd、write.parquet.row-group-size-bytes = 67108864、write.parquet.page-row-limit = 20000
列名 大文字のまま。並びは STATUS が先頭で、ソースの定義順とは異なる

表プロパティのうち zstd と write.parquet.page-row-limit は Apache Iceberg の既定値と同じで、write.object-storage.enabled と write.parquet.row-group-size-bytes は既定と違うので、Data Transforms が設定した値です。

Oracle の列の型と Iceberg テーブルの型の対応は次のとおりです。
主キー列は required になり、NUMBER(10) は decimal(10, 0) になります。
SQL で書き出した CUSTOMERS では NUMBER(10) の列を INT と宣言したので、同じ型の列でも手段によって Iceberg の型が変わります。

Oracle の列 Iceberg の型
ORDER_ID NUMBER(10) NOT NULL(主キー) decimal(10, 0)、required
CUSTOMER_ID NUMBER(10) decimal(10, 0)
ORDER_DATE DATE date
STATUS VARCHAR2(20) string
TOTAL_AMOUNT NUMBER(12,2) decimal(12, 2)
UPDATED_AT TIMESTAMP timestamp

6. ADB から Iceberg テーブルを読み戻す

書き出した表を ADB の SQL からも読み戻してみます。
DBMS_CATALOG はマウントしたカタログのメタデータ(スキーマと表の一覧)を既定で 3600 秒キャッシュするので、期間内に外部で作られた表を見るには FLUSH_CATALOG_CACHE でキャッシュを消します。
Data Transforms が作った表は、キャッシュを消すまで ADB 側の一覧に出ませんでした。

-- 外部で作られた表を見えるようにしてから、件数とステータス別の集計を取る
EXEC DBMS_CATALOG.FLUSH_CATALOG_CACHE('AICAT');
SELECT table_name FROM DBMS_CATALOG.GET_TABLES('AICAT', 'SALES');
SELECT COUNT(*) FROM "SALES"."ORDERS"@AICAT;
SELECT status, COUNT(*) AS cnt, SUM(total_amount) AS amt FROM "SALES"."ORDERS"@AICAT GROUP BY status ORDER BY status;

件数は 1,000 で、ステータス別の件数と金額の合計はローカル表と一致しました。

c29-dbactions-orders-compare.jpg

7. 差分ロードを設定する

全量ロードのあとは、ソースで増えた行や変わった行だけを送る差分ロードを試します。
差分だけを送れば、毎回全量を書き直さずに済みます。

確かめるのは主に次の 2 点です。
「増分追加で新しい注文明細だけが追加されるか」、「増分マージで更新と挿入が既存の表に反映されるか。」

あわせて、それぞれが Iceberg のスナップショットとファイルにどう現れるかを、metadata.json とマニフェストで見ます。

新しいデータ・ロード LOAD_ITEMS_INCR を同じソースとロード先で作り、表ごとに Target Action を設定します。

増分列には、行を追加したり更新したりするたびに進む値を使います。

ORDER_ITEMS は「増分追加」で、Incremental Column(増分列)に ORDER_ID を選びます。

CUSTOMERS_M は「増分マージ」で、Incremental Column に UPDATED_AT、Merge Key に CUSTOMER_ID を選びます。

増分列の候補にはキー列に (Key) が付いて表示されます。

「検証」で「有効」と出たら保存します。

c31-dt-incr-settings.jpg

8. 増分追加と増分マージを実行する

1 回目の実行は全量と同じ動きで、ORDER_ITEMS に 3,000 行、CUSTOMERS_M に 200 行が append のスナップショットで書かれました。
ここでソースに変更を加えます。

-- 増分追加の確認用:注文 10 件(1001〜1010)と明細 30 行を追加
INSERT INTO orders (order_id, customer_id, order_date, status, total_amount, updated_at)
SELECT 1000 + level, MOD(level, 200) + 1, DATE '2026-10-08', 'NEW', 5000 + level, SYSTIMESTAMP
FROM dual CONNECT BY level <= 10;
INSERT INTO order_items (order_id, line_no, product, quantity, unit_price)
SELECT o.order_id, n.line_no, 'ヘッドセット', 1, 9800
FROM orders o CROSS JOIN (SELECT level AS line_no FROM dual CONNECT BY level <= 3) n
WHERE o.order_id > 1000;

-- 増分マージの確認用:既存 5 行を更新(与信枠を +10000)、新規 3 行を追加
UPDATE customers_m SET credit_limit = credit_limit + 10000, updated_at = SYSTIMESTAMP WHERE customer_id IN (1, 2, 3, 4, 5);
INSERT INTO customers_m (customer_id, customer_name, email, prefecture, registered_on, created_at, credit_limit, updated_at)
SELECT 200 + level, '顧客' || LPAD(200 + level, 4, '0'), 'customer' || (200 + level) || '@example.com', '東京都', DATE '2026-10-08', SYSTIMESTAMP, 100000, SYSTIMESTAMP
FROM dual CONNECT BY level <= 3;
COMMIT;

2 回目を実行すると、ORDER_ITEMS は 30 行が挿入され、CUSTOMERS_M は 0 行でした。

c33-dt-incr-run2-job27-summary.jpg

CUSTOMERS_M が 0 行だった原因は、ソースで付けた UPDATED_AT の小数秒でした。
Data Transforms が生成した SQL では、増分列が「前回の上限より大きく、今回の上限以下」の行をソースから抽出していました。
今回の上限はソースの最大値から求めますが、TIMESTAMP の列では CAST(MAX(UPDATED_AT) AS DATE) で秒までに切り捨てられていました。
SYSTIMESTAMP で付けた 8 行はすべて 14:52:51 台の小数秒を持っていたので、上限の 14:52:51.0 を超えて対象外になりました。
ADB の v$sql に残っていた抽出条件はこうなっていました。

-- 2 回目の CUSTOMERS_M の抽出条件(Data Transforms が生成した SQL の WHERE 句)
WHERE
  (CUSTOMERS_M.UPDATED_AT >  TO_TIMESTAMP('2024-01-14 20:20:00.0', 'YYYY-MM-DD HH24:MI:SS.FF')  and 
CUSTOMERS_M.UPDATED_AT <= TO_TIMESTAMP('2026-10-08 14:52:51.0', 'YYYY-MM-DD HH24:MI:SS.FF')
)

前回の上限はデータ・ロードが保持していて、次の実行の下限になります。
ドキュメントにも、増分列の最終更新値はデータ・ロードに格納されると書かれています。
ソースを変えずに実行した 3 回目は、両方の表が 0 行で、スナップショットは作られませんでした。

注意点: 増分列に TIMESTAMP を使うときは、秒未満を持たない値を入れます。
秒未満を持つ行のうち、最大値と同じ秒にあるものは、それより後の秒の行が現れるまで次回に持ち越されます。
ORDER_ID のような連番の NUMBER 列では、この切り捨ては起きません。

UPDATED_AT を秒未満のない値に付け直してから 4 回目を実行すると、CUSTOMERS_M は 8 行が対象になり、挿入 3、更新 5 でした。

-- 変更した 8 行の UPDATED_AT を秒単位の値に付け直す
UPDATE customers_m SET updated_at = CAST(SYSTIMESTAMP AS TIMESTAMP(0)) WHERE customer_id IN (1, 2, 3, 4, 5, 201, 202, 203);
COMMIT;

c38-dt-incr-run4-job41-merge.jpg

CUSTOMERS_M のステップを展開すると、Init catalog、Create target table、Merge rows の順にタスクが並び、既存の表に対する Create target table は 1 秒で終わっていました。

c41-dt-incr-run4-job41-tasks.jpg

4 回の実行をまとめると次のようになります。

実行 ソースの状態 ORDER_ITEMS(増分追加) CUSTOMERS_M(増分マージ)
1 回目 初期データ 3,000 行。append のスナップショット 200 行。append のスナップショット
2 回目 明細 30 行を追加。顧客 5 行を更新、3 行を追加(小数秒つき) 30 行。append のスナップショット 0 行。スナップショットなし
3 回目 変更なし 0 行。スナップショットなし 0 行。スナップショットなし
4 回目 顧客 8 行の UPDATED_AT を秒単位に付け直し 0 行 8 行(更新 5、挿入 3)。overwrite のスナップショット

9. Iceberg のスナップショットとファイルの変化を見る

ここからは Iceberg 側の変化です。

増分追加の 2 回目では、30 行の Parquet が 1 つ(1,941 バイト)、マニフェストが 1 つ、マニフェストリストが 1 つ、metadata.json が 2 世代増えました。

新しいスナップショットは operation = append、added-records 30 で、そのマニフェストリストは 1 回目のマニフェスト(3,000 行)と 2 回目のマニフェスト(30 行)の両方を指します。
total-data-files は 2 になり、delete file はありません。

増分マージの 4 回目は、スナップショットの operation が overwrite でした。

マニフェストを読むと、1 回目のデータファイル(200 行)が DELETED になり、書き直した 200 行のファイル(更新した 5 行と変更のない 195 行)と、挿入した 3 行のファイルが ADDED になっていました。
position delete や equality delete のファイルは作られていません。
更新を含むデータファイルを丸ごと書き直す、copy-on-write の書き方です。

項目 1 回目(append) 4 回目(overwrite)
added-records / deleted-records 200 / 0 203 / 200
added-data-files / deleted-data-files 1 / 0 2 / 1
total-delete-files 0 0
マニフェストの内容 ADDED 1 ファイル(200 行) DELETED 1 ファイル(200 行)、ADDED 2 ファイル(200 行と 3 行)

なお、4 回目でバケットに書かれたデータファイルは 6 個で、スナップショットから参照されるのは 2 個でした。

ADB のドキュメントでは、delete file を参照する Iceberg テーブルへの問合せは失敗すると説明されていますが、この書き方なら読めます。
4 回目のあとに ADB から確かめると、203 行で、更新した与信枠と挿入した行が反映されていました。

EXEC DBMS_CATALOG.FLUSH_CATALOG_CACHE('AICAT');
SELECT COUNT(*) AS cust_cnt, MAX(customer_id) AS max_id, MAX(updated_at) AS max_upd FROM "SALES"."CUSTOMERS_M"@AICAT;
SELECT customer_id, credit_limit, updated_at FROM "SALES"."CUSTOMERS_M"@AICAT WHERE customer_id IN (1, 2, 3, 4, 5, 6, 201, 202, 203) ORDER BY customer_id;

c40-dbactions-incr-run4-rows.jpg

10. Snowflake から参照する

Data Transforms が作った表も、SQL で書き出した表と同じ AICAT の名前空間にあるので、Snowflake 側の設定は前の記事と同じです。
外部ボリュームとカタログ統合を作ったうえで、読み取り専用のカタログリンクデータベースを作ると、SALES の 4 表が作成の約 1 秒後に現れました(1 回の実測)。

CREATE DATABASE aicat_linked_db LINKED_CATALOG = (CATALOG = 'aicat_rest_int', ALLOWED_NAMESPACES = ('SALES'), ALLOWED_WRITE_OPERATIONS = NONE, SYNC_INTERVAL_SECONDS = 30) EXTERNAL_VOLUME = 'oci_aicat_ext_vol';
SHOW ICEBERG TABLES IN SCHEMA aicat_linked_db.SALES;
SELECT COUNT(*) FROM aicat_linked_db.SALES.ORDERS;
SELECT COUNT(*) FROM aicat_linked_db.SALES.ORDER_ITEMS;
SELECT * FROM aicat_linked_db.SALES.CUSTOMERS_M WHERE customer_id < 4;

c59-snowflake-linked-db-tables.jpg

件数は ADB 側と同じで、ORDERS が 1,000 行、ORDER_ITEMS が 3,030 行、CUSTOMERS_M が 203 行でした。

増分マージで書き直した顧客 1〜3 の与信枠も、Snowflake から読んだ値は 61234、62468、63702 で一致します。

Data Transforms が付ける write.object-storage.enabled のハッシュ付きのデータパスも、copy-on-write で書き直したファイルも、Snowflake 側で特別な設定は要りませんでした。

c64-snowflake-linked-db-customers-m-rows.jpg

まとめ

使うときの注意点です。

  • Apache Iceberg 接続はロード先専用で、ソース接続のタイプには出ない
  • 自動で用意された ADB の接続には資格証明がなく、設定するまでデータ・ロードのエディタで ORA-01017 になる。パスワードは入力してすぐ「更新」する
  • Data Transforms が作った表を ADB の SQL で見るには、DBMS_CATALOG.FLUSH_CATALOG_CACHE でカタログのキャッシュを消す
  • 増分列が TIMESTAMP のとき、今回の上限は秒までに切り捨てられる。秒未満を持つ行は、より後の秒の行が現れるまで持ち越される(生成された SQL の観察)
  • 増分列の前回の上限はデータ・ロードごとに保持される。同じ表に新しいデータ・ロードを作ると、前回の上限は引き継がれず初回扱いになる(ドキュメントの注記。実機では未確認)
  • ターゲットを空にしていない状態で「追加」から「増分マージ」に切り替えると、既存の全行が更新される(ドキュメントの注記。実機では未確認)
  • Iceberg ターゲットの 4 つの Target Action に、ソース側の削除を反映する選択肢はない
  • 増分マージは copy-on-write で書かれ、delete file は作られない(今回の環境での観察)
  • 増分マージの実行で、スナップショットから参照されないデータファイルが 4 個残った(6 個書かれ、参照されたのは 2 個)。後で掃除されるかは確認していない

Data Transforms の画面から、ローカル表を AICAT 管理の Iceberg テーブルへ全量ロードし、増分追加と増分マージで差分を送るところまでを確かめました。
増分追加は append のスナップショットとデータファイルが 1 つずつ増え、増分マージは overwrite のスナップショットでデータファイルを書き直す動きでした。
増分列の秒未満の扱いは、ドキュメントに記述がない分、増分列を決めるときに押さえておくと差分が 0 件になる場面を避けられます。
少しでも参考になれば嬉しいです。

参考

4
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?