11
4

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】ADB の Iceberg テーブルを AICAT経由で Snowflake から参照する

11
Last updated at Posted at 2026-10-12

image.png

Oracle Database にあるデータを、Snowflake のような別の分析基盤からも読みたい場面があります。
Autonomous AI Database には Oracle AI Data Catalog という Iceberg REST カタログが用意されていて、SQL で書き出した Apache Iceberg のテーブルを、カタログ経由で他のエンジンに公開できます。
ADB の持つデータを Iceberg テーブルとして Object Storage に書き出し、別のエンジン(今回は Snowflake)から参照させると、Oracle から Snowflake へデータをコピーする仕組みを別に作らなくても、Snowflake の SQL で同じデータを読めます。
データの実体は Object Storage に 1 つで、Oracle 側で COMMIT した追加は、Snowflake 側の問い合わせにもそのまま反映されます。
Apache Iceberg は複数のエンジンから同じテーブルを扱うことを前提にしたオープンなテーブル形式なので、参照する側は Snowflake に限りません。
この記事では、ADB(Autonomous AI Database)のデータをSnowflakeから参照させるために、カタログの有効化から、SQL での書き出し、Snowflake のカタログリンクデータベースで読むところまでを、実際に動かして確かめた手順で紹介します。

書き出しの手段は今回行うSQLを用いた方法のほかに Oracle Data Transforms や OCI GoldenGate があり、それぞれ別の記事で扱います。

前提条件

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

項目 内容
Oracle Database Autonomous AI Lakehouse(Serverless、26ai、ECPU 2、大阪リージョン)。Database Actions の SQL ワークシートを ADMIN で使う
OCI Object Storage ADB と同じテナンシ、同じ大阪リージョンに作った専用のバケット。Iceberg テーブルの実体(Parquet とメタデータ)を置く
Snowflake トライアルのアカウント(AWS、Asia Pacific Tokyo)。Web UI の Snowsight を ACCOUNTADMIN で使う
扱うデータ ADB の中に作る顧客表 CUSTOMERS(200 行)。NUMBER、VARCHAR2、DATE、TIMESTAMP の列を持つ普通の Oracle の表で、これを Iceberg テーブルとして書き出す

この記事で ローカル表 と書くのは、ADB の中にある Oracle Databaseの表のことです。
Iceberg テーブルは、ADB の外の Object Storage にファイルとして置かれ、カタログで管理されます。

用語を 4 つ先に書いておきます。

  • Iceberg REST カタログ は、Iceberg テーブルの名前と最新のメタデータファイルの場所を REST API で管理する仕組みです。
  • Snowflake の 外部ボリューム は、Snowflake がオブジェクトストレージを読み書きするための接続先の定義です。
  • Snowflake の カタログ統合 は、外部のカタログへの接続先と認証を定義するオブジェクトです。
  • Snowflake の カタログリンクデータベース は、外部のカタログにある名前空間とテーブルを、Snowflake のデータベースとして自動で同期する機能です。

DBMS_CLOUD.EXPORT_DATA でも Object Storage に Parquet を出力できますが、ドキュメント上の出力形式は CSV、JSON、XML、Parquet、Data Pump で、Iceberg テーブルとして書き出す(メタデータを作ってカタログに登録する)形式はありません。
こちらは試していないので、この記事では扱いません。

構成

構成は次のとおりです。 ADB と Object Storage は OCI の大阪リージョンにあり、Oracle AI Data Catalog(以下 AICAT)は ADB の上で動きます。 ADB は SQL で Parquet とメタデータを Object Storage に書き、テーブルの作成とコミットを AICAT に記録します。 Snowflake は AWS 上のアカウントで、OCI の外からカタログ統合で AICAT に表の場所を聞き、外部ボリュームで Object Storage のファイルを読みます。

image.png

Oracle AI Data Catalog の概要

AICAT は、Autonomous AI Database の上で動く Oracle 管理の Iceberg REST カタログです。
ドキュメントでは、カタログの計算資源は使っている間だけ ECPU が課金され、2 時間使わなければ止まる設計で、初期リリースでは最大 32 ECPU と説明されています。
Oracle はテナンシに 1 つ、Autonomous AI Lakehouse で有効化することを推奨していますが、ほかの ADB で有効化することもできます。
認証は OAuth2 の client_credentials で、DB ユーザーとパスワードでトークンを取ります。
トークンの有効期限は 1 時間です。

手順

それでは実際に進めていきましょう。
1〜5 が AICAT の準備、6〜11 が ADB の SQL での書き出しと確認、12〜16 が Snowflake 側の操作です。

1. OCI Object Storage のバケットと顧客秘密キーを用意する

Iceberg テーブルの実体を置くバケットを、ADB と同じテナンシの OCI Object Storage に作ります。
AICAT と Snowflake は、どちらもこのバケットを S3 互換 API で使います。
S3 互換 API は、OCI Object Storage を Amazon S3 向けのクライアントやライブラリから使うための API で、顧客秘密キーで認証します。

S3 互換 API のエンドポイントは 2 種類あります。
「path style」 はホスト名の先頭に Object Storage のネームスペースが付く形で、大阪リージョンなら https://<Object Storage のネームスペース>.compat.objectstorage.ap-osaka-1.oci.customer-oci.com です。
「virtual-hosted style」 はホスト名の先頭にバケット名が付く形で、設定にはバケット名を含めない vhcompat.objectstorage.ap-osaka-1.oci.customer-oci.com を書きます。
AICAT の登録には path style を使い、Snowflake は virtual-hosted style だけを受け付けます。

次の内容でバケットを作ります。

  • 名前 — 例では icebergsnowflake-aicat。専用の空のバケットにします
  • リージョン — ADB と同じ大阪リージョン
  • バケット・スコープ — 「リージョン」にします。※既定の「ネームスペース」のままだと、virtual-hosted style では NoSuchBucket になりました

バケット・スコープは作成後にも OCI CLI で変えられます。

# 既存のバケットのスコープをリージョンに変える
oci os bucket update --bucket-name icebergsnowflake-aicat --bucket-scope REGION

次に、OCI のユーザー設定で「顧客秘密キー」を作り、アクセスキーとシークレットを控えます。
シークレットは作成時にしか表示されません。
作成手順は以下の記事をご覧ください。

(今回は、作成から約 9 分後にホーム・リージョン以外の大阪で使えるようになりました。)

2. AICAT を有効化する

有効化は、ADB にフリーフォーム・タグ ADB$TOOLS = AI_CAT を付けるだけです。
OCI コンソールの ADB の詳細画面からタグを追加するか、OCI CLI で付けます。

# ADB にタグを付けて AICAT を有効化する
# --freeform-tags は既存のフリーフォーム・タグを置き換えるので、ほかのタグがある場合はまとめて指定する
# 確認プロンプトが出るので、スクリプトから実行するなら --force を付ける
oci db autonomous-database update --autonomous-database-id <ADB の OCID> --freeform-tags '{"ADB$TOOLS": "AI_CAT"}'

コンソールでの有効化から、バケットの作成、ストレージの登録、マウントまでの画面は、以前の記事に書いていますので、詳しくはこちらをご覧ください。

カタログの URL は、Database Actions と同じホスト名に /catalog を付けたものです。
ブラウザで開くと、カタログ DB の指名、ストレージの登録、ユーザーのオンボードの 3 ステップが案内されます。
指名はタグ付けに対応し、オンボードはドキュメントでは AICAT_USER か PDB_DBA ロールの付与(ADMIN はそのまま使える)とされています。
今回は ADMIN に AICAT_USER を付与した状態で進めました。

c06-aicat-splash.jpg

3. AICAT の REST が応答することを確認する

有効化できたかは、トークンの取得で確かめます。
トークンは curl で取れます。
ストレージを登録するまで、カタログの API は 503 を返します。

# トークンの取得(client_id は DB ユーザー、client_secret はそのパスワード)
curl -s -X POST "https://<ADB のホスト名>/catalog/v1/auth/token" -H "Content-Type: application/x-www-form-urlencoded" --data-urlencode "grant_type=client_credentials" --data-urlencode "client_id=ADMIN" --data-urlencode "client_secret=<パスワード>" --data-urlencode "scope=PRINCIPAL_ROLE:ALL"
# 応答:access_token、expires_in 3600、token_type bearer、refresh_token

# ストレージ登録前の /v1/config は 503
# {"error":{"message":"Oracle AI Data Catalog is unavailable because storage metadata is not registered", ... "type":"StorageMetadataNotRegistered"}}

4. ネットワーク ACL を設定する

ADB の SQL からカタログと S3 互換エンドポイントに HTTPS で到達できるよう、Database Actions の SQL ワークシートでネットワーク ACL を付けます。
*.oraclecloudapps.com が AICAT、*.oci.customer-oci.com が Object Storage の S3 互換エンドポイントです。

-- ADB からの HTTPS 接続先を許可する(ADMIN に付与)
BEGIN
  FOR h IN (SELECT COLUMN_VALUE AS host FROM TABLE(SYS.ODCIVARCHAR2LIST('*.oraclecloudapps.com', '*.oci.customer-oci.com'))) LOOP
    DBMS_NETWORK_ACL_ADMIN.APPEND_HOST_ACE(
      host       => h.host,
      lower_port => 443,
      upper_port => 443,
      ace        => xs$ace_type(privilege_list => xs$name_list('http'),
                                principal_name => 'ADMIN',
                                principal_type => xs_acl.ptype_db));
  END LOOP;
END;
/

5. AICAT に Object Storage を登録する

同じ SQL ワークシートで ORACLE_AI_DATA_CATALOG.REGISTER_STORAGE_OCI を実行します。
p_warehouse には 1 で作ったバケットを s3:// の形式で渡し、エンドポイントは path style、リージョンと顧客秘密キーを渡します。

-- AICAT にストレージを登録する
BEGIN
  ORACLE_AI_DATA_CATALOG.REGISTER_STORAGE_OCI(
    p_warehouse  => 's3://icebergsnowflake-aicat',
    p_endpoint   => 'https://<Object Storage のネームスペース>.compat.objectstorage.ap-osaka-1.oci.customer-oci.com',
    p_region     => 'ap-osaka-1',
    p_access_key => '<顧客秘密キーのアクセスキー>',
    p_secret_key => '<顧客秘密キーのシークレット>'
  );
END;
/

登録後は GET /catalog/v1/config が 200 を返します。
応答には、REST のパスに使う prefix が aicat_iceberg_rest_catalog、資格証明ベンディングが "false" と入っています。
資格証明ベンディングは、カタログがストレージの一時的な資格証明を読み手に配る仕組みです。
応答では無効になっているので、読み手は Object Storage の顧客秘密キーを自分で持ちます。

{"defaults":{"default-base-location":"s3://icebergsnowflake-aicat"},"overrides":{"prefix":"aicat_iceberg_rest_catalog","credential-vending":"false"},"endpoints":["GET /v1/{prefix}/namespaces", ...]}

6. ADB にローカル表を作る

書き出す元になる顧客表 CUSTOMERS を、ADB の ADMIN スキーマに作ります。 Oracle の列の型が Iceberg のどの型になるかを見たいので、NUMBER、VARCHAR2、DATE、TIMESTAMP の列をそろえ、200 行を生成します。
列は、顧客 ID、顧客名、メールアドレス、都道府県、登録日、作成日時、与信枠の 7 つです。 与信枠(credit_limit)は、顧客ごとに認める取引額の上限のつもりで入れた金額の列で、小数 2 桁の NUMBER が Iceberg のどの型になるかを見るために使います。

(同じスクリプトで注文表と注文明細表も作りましたが、この記事で使うのは顧客表だけです。)

CREATE TABLE customers (
  customer_id    NUMBER(10)     NOT NULL,
  customer_name  VARCHAR2(100)  NOT NULL,
  email          VARCHAR2(200),
  prefecture     VARCHAR2(20),
  registered_on  DATE,
  created_at     TIMESTAMP,
  credit_limit   NUMBER(12,2),
  CONSTRAINT customers_pk PRIMARY KEY (customer_id)
);

-- 顧客 200 行
INSERT INTO customers (customer_id, customer_name, email, prefecture, registered_on, created_at, credit_limit)
SELECT level,
       '顧客' || LPAD(level, 4, '0'),
       'customer' || level || '@example.com',
       CASE MOD(level, 5) WHEN 0 THEN '東京都' WHEN 1 THEN '大阪府' WHEN 2 THEN '愛知県' WHEN 3 THEN '福岡県' ELSE '北海道' END,
       DATE '2024-01-01' + MOD(level * 7, 600),
       TIMESTAMP '2024-01-01 09:00:00' + NUMTODSINTERVAL(level * 97, 'MINUTE'),
       ROUND(50000 + MOD(level * 1234, 450000), 2)
FROM dual CONNECT BY level <= 200;
COMMIT;

7. 資格証明を作る

ADB の SQL から AICAT を使うには、カタログ用とストレージ用の 2 つの資格証明が要ります。
カタログ用は、AICAT の OAuth2 トークンを自動で取得して更新する Bearer トークンの資格証明です。
ストレージ用は、1 で作った顧客秘密キーです。

-- カタログ用:AICAT の OAuth2 トークンを自動で取得・更新する資格証明
BEGIN
  DBMS_SHARE.CREATE_BEARER_TOKEN_CREDENTIAL(
    CREDENTIAL_NAME => 'AICAT_TOKEN_CRED',
    BEARER_TOKEN    => 'BEARER_TOKEN',
    TOKEN_ENDPOINT  => 'https://<ADB のホスト名>/catalog/v1/auth/token',
    CLIENT_ID       => 'ADMIN',
    CLIENT_SECRET   => '<パスワード>',
    TOKEN_SCOPE     => 'PRINCIPAL_ROLE:ALL');
END;
/

-- ストレージ用:顧客秘密キー
BEGIN
  DBMS_CLOUD.CREATE_CREDENTIAL(
    credential_name => 'AICAT_S3_CRED',
    username        => '<アクセスキー>',
    password        => '<シークレット>');
END;
/

Bearer トークンの資格証明を作ると、リフレッシュ用の資格証明も自動で追加されます。

8. AICAT をマウントしてスキーマを作る

AICAT をカタログとしてマウントします。
マウントは、カタログの表を @AICAT のように名前に付けて SQL から参照できるようにする操作です。
続けて、Iceberg テーブルを入れる名前空間として SALES スキーマを作ります。

-- AICAT を ICEBERG_ORACLE としてマウントする
BEGIN
  DBMS_CATALOG.MOUNT_ICEBERG(
    catalog_name            => 'AICAT',
    endpoint                => 'https://<ADB のホスト名>/catalog/v1',
    catalog_credential      => 'AICAT_TOKEN_CRED',
    data_storage_credential => 'AICAT_S3_CRED',
    catalog_type            => 'ICEBERG_ORACLE');
END;
/
SELECT catalog_name, catalog_type FROM user_mounted_catalogs ORDER BY catalog_name;

-- 名前空間 SALES を作る
BEGIN
  DBMS_CATALOG.CREATE_SCHEMA(catalog_name => 'AICAT', schema_name => 'SALES');
END;
/
SELECT schema_name FROM DBMS_CATALOG.GET_SCHEMAS('AICAT');

9. Iceberg テーブルを作り、ローカル表から INSERT する

CREATE ICEBERG TABLE で表を作り、INSERT ... SELECT で 6 のローカル表の内容を入れます。
列の型は Iceberg の型で宣言します。

CREATE ICEBERG TABLE "SALES"."CUSTOMERS"
( CUSTOMER_ID   INT,
  CUSTOMER_NAME STRING,
  EMAIL         STRING,
  PREFECTURE    STRING,
  REGISTERED_ON DATE,
  CREATED_AT    TIMESTAMP,
  CREDIT_LIMIT  DECIMAL(12,2)
)
WITHIN CATALOG "AICAT"
STORAGE LOCATION "s3://icebergsnowflake-aicat/SALES/CUSTOMERS/";

INSERT INTO "SALES"."CUSTOMERS"@AICAT
SELECT customer_id, customer_name, email, prefecture, registered_on, created_at, credit_limit
FROM   customers;
COMMIT;

表の作成が 4.1 秒、200 行の挿入が 8.5 秒、コミットが 2.3 秒でした(1 回の実測)。
公式ドキュメントでは、Iceberg テーブルへの INSERT ... SELECT のあとは COMMIT か ROLLBACK が必要で、それまでは対象の表の問い合わせと、ほかの表への DML ができない(ほかの表の問い合わせは可)と説明されています。
開いたトランザクションがあると、挿入そのものがエラーになるとも書かれています。

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

書き出した表を、同じ SQL ワークシートから @AICAT 付きの名前で問い合わせます。

SELECT COUNT(*) FROM "SALES"."CUSTOMERS"@AICAT;
SELECT table_name FROM DBMS_CATALOG.GET_TABLES('AICAT', 'SALES');

200 行が返りました。

c08-dbactions-catalog-count.jpg

都道府県ごとの件数と与信枠(credit_limit 列)の合計を、Iceberg テーブルとローカル表のそれぞれで集計すると、5 つの都道府県とも件数は 40 で、合計も一致しました。

c07-dbactions-aggregate-compare.jpg

バケットには SALES/CUSTOMERS/data/ に Parquet が 1 つ、metadata/ に metadata.json が 2 世代(作成時と挿入後)、マニフェストとマニフェストリストが 1 つずつできていました。

image.png

マニフェストはデータファイルの一覧と統計を持つファイルで、マニフェストリストはスナップショットが参照するマニフェストの一覧です。 metadata.json は format-version 2 で、Parquet の圧縮は zstd(ドキュメントではレベル 3)です。 Oracle の列の型と、CREATE ICEBERG TABLE で宣言した型、できた Iceberg テーブルの型の対応は次のとおりです。

Oracle の列 CREATE ICEBERG TABLE の宣言 Iceberg の型
NUMBER(10) INT int
VARCHAR2(100) STRING string
DATE DATE date
TIMESTAMP TIMESTAMP timestamp
NUMBER(12,2) DECIMAL(12,2) decimal(12, 2)

11. REST で表が解決できることを確認する

Snowflake は、AICAT の REST API を呼んで表を解決します。 Snowflake につなぐ前に、同じ REST API を curl で呼び、名前空間と表の一覧が返ること、表の定義(最新の metadata.json の場所)が取れることを確かめます。 名前空間の一覧は、/v1/config が返した prefix 付きのパスで取ります。

# 名前空間、表の一覧、表の定義(load table)
curl -s "https://<ADB のホスト名>/catalog/v1/aicat_iceberg_rest_catalog/namespaces" -H "Authorization: Bearer <トークン>"
# {"namespaces": [["SALES"]]}
curl -s "https://<ADB のホスト名>/catalog/v1/aicat_iceberg_rest_catalog/namespaces/SALES/tables" -H "Authorization: Bearer <トークン>"
# {"identifiers": [{"namespace": ["SALES"], "name": "CUSTOMERS"}]}
curl -s "https://<ADB のホスト名>/catalog/v1/aicat_iceberg_rest_catalog/namespaces/SALES/tables/CUSTOMERS" -H "Authorization: Bearer <トークン>" -H "X-Iceberg-Access-Delegation: vended-credentials"
# "metadata-location": "s3://icebergsnowflake-aicat/SALES/CUSTOMERS/metadata/00001-....metadata.json"
# "config": {"s3.path-style-access": "true", "s3.endpoint": "https://<Object Storage のネームスペース>.compat.objectstorage.ap-osaka-1.oci.customer-oci.com", "io-impl": "org.apache.iceberg.aws.s3.S3FileIO", "client.region": "ap-osaka-1"}
# "storage-credentials": null

3 つ目の load table(表の定義を返す API)の応答には、最新の metadata.json の場所と、Object Storage のエンドポイントなどの接続情報が入ります。 Object Storage を読むためのキーは入っていません(storage-credentials が null)。 手順 5 で見たとおり AICAT は資格証明ベンディングに対応していないので、Snowflake はファイルの場所をカタログから教えてもらい、Object Storage を読むための顧客秘密キーは外部ボリュームに自分で設定します(手順 13)。

12. Snowflake の S3 互換エンドポイントの有効化

ここからは Snowflake 側です。
Snowflake の S3 互換ストレージは、Cloudflare R2 以外のエンドポイントを使うのに Snowflake Support の有効化が必要です(詳細はこちら)。

Amazon S3-compatible endpoints are not automatically enabled for all accounts. Cloudflare endpoints that contain r2.cloudflarestorage.com are enabled by default. To enable other endpoints, contact Snowflake Support.

外部ステージは、Snowflake の外にあるオブジェクトストレージの場所(バケットとパス)と、その接続先と認証を、Snowflake に登録しておく定義です。 ファイルの一覧や、COPY での読み込みに使います。
有効化前に、確認用のバケットを指す外部ステージを作ると、次のエラーになります。

SQL compilation error: Endpoint vhcompat.objectstorage.ap-osaka-1.oci.customer-oci.com not allowed.

Support ケースには、

  • アカウント識別子
  • クラウドとリージョン
  • エンドポイントのホスト名
  • ベンダー名

を書きます。

image.png

加えて、Snowflake が公開しているテストスイート snowflake-s3compat-api-test-suite の結果を求められました。
1,000 件を超えるオブジェクトの一覧(mvn test -DPAGE_LISTING_TOTAL_SIZE="1005")が通ることが必須条件でした。
OCI Object Storage では 9 件中 6 件が合格で、今回の 3 件の差分(否定テストのエラーコードの違いと、リージョン未指定時の署名エラー)は許容され、依頼から 4 日後に有効化されました。
結果はケースにコメントとして投稿し、ログを添付しました。

image.png

13. Snowflake で外部ボリュームを作る

有効化後は、手順 12 でエラーになった外部ステージの作成と、ファイル一覧の LIST が通るようになっているはずなので確かめます。

-- 確認用のバケット(icebergsnowflake-s3test)を指す外部ステージ
CREATE OR REPLACE STAGE oci_s3compat_trial_stage
  URL = 's3compat://icebergsnowflake-s3test/test/'
  ENDPOINT = 'vhcompat.objectstorage.ap-osaka-1.oci.customer-oci.com'
  CREDENTIALS = (AWS_KEY_ID = '<アクセスキー>' AWS_SECRET_KEY = '<シークレット>');
LIST @oci_s3compat_trial_stage;

確認用のバケットに置いた hello.txt が 1 行返りました。

c42-snowflake-stage-list.jpg

Iceberg テーブルの参照に使うのは外部ボリュームで、S3 互換ではエンドポイントのホスト名と顧客秘密キーを直接書きます。
今回は読み取りだけなので ALLOW_WRITES = FALSE にしました。

USE ROLE ACCOUNTADMIN;
CREATE OR REPLACE EXTERNAL VOLUME oci_aicat_ext_vol
  STORAGE_LOCATIONS = (
    (
      NAME = 'oci_aicat_bucket'
      STORAGE_PROVIDER = 'S3COMPAT'
      STORAGE_BASE_URL = 's3compat://icebergsnowflake-aicat/'
      CREDENTIALS = (AWS_KEY_ID = '<アクセスキー>' AWS_SECRET_KEY = '<シークレット>')
      STORAGE_ENDPOINT = 'vhcompat.objectstorage.ap-osaka-1.oci.customer-oci.com'
    )
  )
  ALLOW_WRITES = FALSE;
SHOW EXTERNAL VOLUMES;
SELECT SYSTEM$VERIFY_EXTERNAL_VOLUME('oci_aicat_ext_vol');

作成した外部ボリュームは SHOW EXTERNAL VOLUMES で確認できます。allow_writes が false になっています。

c43-snowflake-show-ext-volumes.jpg

検証の結果は success が true、storageLocationSelectionResult が PASSED、region が ap-osaka-1 でした。
読み取り専用の外部ボリュームでは、read、list、write、delete の各項目が「UNVERIFIED because the external volume doesn't allow writes」になり、success、storageLocationSelectionResult、region の判定だけが返ります。
ドキュメントでは読み取り専用のときこれらの項目は SKIPPED になると書かれていますが、今回の出力は UNVERIFIED でした。

c45-snowflake-ext-vol-verify-flatten.jpg

14. Snowflake でカタログ統合を作る

カタログ統合は、手順 11 で curl から呼んだ AICAT の REST API への接続先と認証を、Snowflake に登録しておくオブジェクトです。
カタログ統合には、種類に Iceberg REST、認証に OAuth を指定します。
トークンの URL に AICAT の /catalog/v1/auth/token を指定し、client_id に DB ユーザー、スコープに PRINCIPAL_ROLE:ALL を書きます。
CATALOG_NAME と PREFIX は書いていません。

CREATE OR REPLACE CATALOG INTEGRATION aicat_rest_int
  CATALOG_SOURCE = ICEBERG_REST
  TABLE_FORMAT = ICEBERG
  CATALOG_NAMESPACE = 'SALES'
  REST_CONFIG = (
    CATALOG_URI = 'https://<ADB のホスト名>/catalog'
  )
  REST_AUTHENTICATION = (
    TYPE = OAUTH
    OAUTH_TOKEN_URI = 'https://<ADB のホスト名>/catalog/v1/auth/token'
    OAUTH_CLIENT_ID = 'ADMIN'
    OAUTH_CLIENT_SECRET = '<パスワード>'
    OAUTH_ALLOWED_SCOPES = ('PRINCIPAL_ROLE:ALL')
  )
  ENABLED = TRUE
  REFRESH_INTERVAL_SECONDS = 30;

SELECT SYSTEM$VERIFY_CATALOG_INTEGRATION('aicat_rest_int');
SELECT SYSTEM$LIST_NAMESPACES_FROM_CATALOG('aicat_rest_int', '', 1);
SELECT SYSTEM$LIST_ICEBERG_TABLES_FROM_CATALOG('aicat_rest_int', 'SALES', 1);
SELECT SYSTEM$EXECUTE_CATALOG_OPERATION('aicat_rest_int', 'loadTable', 'SALES', 'CUSTOMERS');

作成は 17 秒かかり(1 回の実測)、作成の時点で Snowflake は AICAT にトークンを要求します。

c46-snowflake-catalog-int-show.jpg

検証は {"success": true, "errorCode": "", "errorMessage": ""} でした。

c47-snowflake-catalog-int-verify.jpg

名前空間の一覧は ["SALES"]、表の一覧には CUSTOMERS が出ます(同じ名前空間に別の記事で Data Transforms から作った 3 表もあるので、一覧は 4 表でした)。

c49-snowflake-catalog-list-tables.jpg

loadTable は metadata.json の場所を返し、外部ボリュームのベース URL の配下にあることが確かめられます。

c48-snowflake-catalog-loadtable.jpg

注意点:
SYSTEM$LIST_NAMESPACES_FROM_CATALOG の第 3 引数は、名前空間を何階層までたどるかの指定です。
0 にすると Snowflake はすべての階層をたどろうとして、AICAT に「SALES の下にある名前空間」を問い合わせます。
ところが AICAT の名前空間の一覧は最上位の 1 階層だけで、下の階層を問い合わせても最上位の一覧(SALES)を返します。
このため Snowflake は SALES の下にまた SALES があるものとして問い合わせを繰り返し、3 分半後に「Too many requests need to be sent for the REST Catalog integration」で失敗しました。
第 3 引数は 1(指定した階層の直下だけ)にします。

15. Snowflake から Iceberg テーブルを参照する

ではいよいよ、SnowflakeからIcebergテーブルを参照していきましょう。

① 表ごとに外部管理の Iceberg テーブル(メタデータの管理を外部のカタログに任せる表)を作る方法
② カタログリンクデータベースで自動同期する方法
の 2 つを試しました。

①表ごとにIcebergテーブルを作る

表ごとに作る場合は、外部ボリューム、カタログ統合、カタログ上の表名と名前空間を指定します。

CREATE OR REPLACE ICEBERG TABLE customers
  EXTERNAL_VOLUME = 'oci_aicat_ext_vol'
  CATALOG = 'aicat_rest_int'
  CATALOG_TABLE_NAME = 'CUSTOMERS'
  CATALOG_NAMESPACE = 'SALES'
  AUTO_REFRESH = TRUE;
SELECT COUNT(*) FROM customers;

件数は 200 で、ADB 側と一致しました。先頭の 5 行を見ると、日本語の顧客名もそのまま読めています。

c51-snowflake-customers-first5.jpg

②カタログリンクデータベースで自動同期する

カタログリンクデータベースは、名前空間を絞り、読み取り専用にして作ります。

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';
SELECT SYSTEM$CATALOG_LINK_STATUS('aicat_linked_db');
SHOW ICEBERG TABLES IN SCHEMA aicat_linked_db.SALES;
SELECT COUNT(*) FROM aicat_linked_db.SALES.CUSTOMERS;

作成の文は 1 つで、「Database AICAT_LINKED_DB successfully created.」が返ります。

c56-snowflake-linked-db-created.jpg

作成の約 1 秒後に SALES スキーマと表が現れました(1 回の実測)。

c59-snowflake-linked-db-tables.jpg

SYSTEM$CATALOG_LINK_STATUS は executionState が RUNNING で、failureDetails は空でした。

c57-snowflake-linked-db-status.jpg

16. 更新の反映を確認する

ADB 側で 5 行ずつ 3 回追加して、Snowflake の customers(AUTO_REFRESH = TRUE)に出るまでの時間を測りました。

-- ADB 側で実行(5 行追加してコミット)
INSERT INTO "SALES"."CUSTOMERS"@AICAT SELECT 200 + level, '顧客' || LPAD(200 + level, 4, '0'), 'customer' || (200 + level) || '@example.com', '東京都', DATE '2026-10-12', CAST(SYSTIMESTAMP AS TIMESTAMP), 100000 FROM dual CONNECT BY level <= 5;
COMMIT;
-- Snowflake 側で繰り返し実行
SELECT CURRENT_TIMESTAMP() AS ts, COUNT(*) AS cnt FROM customers;

3 回とも 1 分以内に反映され、コミットから最初に件数が増えた問い合わせまでの時間は 55 秒以内、23 秒以内、19 秒以内でした。 件数を見る問い合わせは手で数十秒おきに実行したので、実際に反映された時刻は、その前の問い合わせからこの問い合わせまでの間のどこかです。 例えば 1 回目は、コミットの 12 秒後の問い合わせではまだ 200 で、55 秒後の問い合わせで 205 になっていました。 55 秒、23 秒、19 秒は「遅くともこの時間までに反映された」という値です。 カタログ統合の REFRESH_INTERVAL_SECONDS を 30 秒にしているので、30 秒間隔のポーリングで追従できている範囲です。 3 回の追加のあと、Snowflake で見た件数は 215 でした。

c62-snowflake-count-after3runs.jpg

カタログリンクデータベース経由でも、追加した 201〜215 の行が読めます。

-- Snowflake 側で実行(追加した行をカタログリンクデータベース経由で読む)
SELECT customer_id, customer_name, prefecture, registered_on
FROM   aicat_linked_db.SALES.CUSTOMERS
WHERE  customer_id > 200
ORDER  BY customer_id;

c67-snowflake-linked-db-added-rows.jpg

まとめ

使うときの注意点です。

  • Snowflake の virtual-hosted style で OCI Object Storage を使うには、バケット・スコープをリージョンにする。既定のネームスペース・スコープでは NoSuchBucket になる
  • 作成直後の顧客秘密キーは、ホーム・リージョン以外への反映に数分かかる
  • OCI の S3 互換 API に boto3 1.36 以降の既定(aws-chunked の転送エンコーディング)でアップロードすると NotImplemented になった。クライアント側の設定で回避できる。Snowflake からの読み取りだけの今回の構成では問題にならなかった
  • Snowflake の S3 互換エンドポイントは Cloudflare R2 以外、Support の有効化が要る。1,000 件超のページング検証が必須条件で、今回は依頼から有効化まで 4 日だった
  • 現時点でAICAT は資格証明ベンディングに対応していない。読み手は Object Storage の顧客秘密キーを自分で持つ
  • AICAT の認証失敗は、パスワードの誤りでも ADB の停止中でも同じ unauthorized_client が返る。Snowflake のカタログ統合は作成の時点でトークンを取りに行くので、ADB が止まっていると作成に失敗する
  • Snowflake の SYSTEM$LIST_NAMESPACES_FROM_CATALOG は第 3 引数を 1(直下だけ)にする

AICAT の有効化はタグ 1 つで、ストレージの登録と SQL の書き出しは Database Actions だけで進みました。 Snowflake 側はS3 互換エンドポイントの有効化する必要がありますが、有効化されればカタログ統合と外部ボリュームの 2 つの定義で表が見えました。

これで、Oracle から Snowflake へデータをコピーする仕組みを持たずに、Oracle 側で COMMIT した内容を Snowflake の SQL からそのまま読む構成になりました。 データの実体は Object Storage の Iceberg テーブル 1 つなので、読み手を Snowflake 以外の Iceberg 対応エンジンに広げるときも、同じカタログと同じファイルを指すだけです。 少しでも参考になれば嬉しいです。

参考

11
4
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
11
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?