Snowflake ストレージを使用する Iceberg テーブルの DuckDB からの参照を試してみる

Snowflake ストレージを使用する Iceberg テーブルの DuckDB からの参照を試してみる

2026.05.22

はじめに

2026年4月のアップデートで、Apache Iceberg テーブル用の Snowflake ストレージがパブリックプレビューとなりました。

https://docs.snowflake.com/en/release-notes/2026/other/2026-04-14-iceberg-snowflake-storage

外部のクエリエンジン(DuckDB)からの参照を試してみた内容を本記事でまとめます。

アップデートの概要

本機能については以下に記載があります。

https://docs.snowflake.com/en/user-guide/tables-iceberg-internal-storage

https://www.snowflake.com/en/blog/storage-iceberg-tables/

これまで Iceberg テーブルを作成するには、外部クラウドストレージ(S3 など)を準備し、外部ボリュームとして登録する必要がありました。本機能により、ストレージの構成・運用を Snowflake に委ねた上で Iceberg テーブルを利用できるようになります。

具体的には、外部クラウドストレージの準備や外部ボリュームの登録が不要でEXTERNAL_VOLUME = 'SNOWFLAKE_MANAGED'を指定するだけで Iceberg テーブルを作成できます。永続的な Iceberg テーブルは Snowflake のフェイルセーフによる保護対象にもなります。

Snowflake をカタログとして使用

Snowflake をカタログとして使用するので、Snowflake 側がメタデータのライフサイクルを管理し、テーブルデータとスナップショットの保持期間に基づいて、古いメタデータ・マニフェストリスト・マニフェストファイルが自動的に削除されます。外部カタログを使う場合に必要となるメタデータ管理の運用負荷を軽減できます。

https://docs.snowflake.com/en/user-guide/tables-iceberg-metadata#tables-that-use-snowflake-as-the-catalog

外部エンジンからのアクセス

Snowflake Horizon Catalog には Apache Polaris が統合されており、Horizon Catalog は Apache Iceberg REST API(Horizon Iceberg REST Catalog API)を公開しています。

https://<account_identifier>.snowflakecomputing.com/polaris/api/catalog

Snowflake ストレージを使用する Apache Iceberg テーブル用も Snowflake Horizon Catalog 経由で外部クエリエンジンからアクセスできます。

https://docs.snowflake.com/en/user-guide/tables-iceberg-access-using-external-query-engine-snowflake-horizon

試してみる

前提条件

本記事では以下の環境を使用しています。

  • Snowflake トライアルアカウント(AWS / ap-northeast-1)
  • DuckDB v1.5.2(外部エンジン検証用)

Iceberg V2 テーブルの作成

はじめにEXTERNAL_VOLUME = 'SNOWFLAKE_MANAGED'を指定して Iceberg V2 テーブルを作成します。Snowflake ストレージを使用するので、従来のような外部ボリュームの事前作成は不要です。

CREATE SCHEMA IF NOT EXISTS TEST_DB.ICEBERG_VERIFICATION;
USE SCHEMA TEST_DB.ICEBERG_VERIFICATION;

CREATE OR REPLACE ICEBERG TABLE iceberg_v2_test (
    id INT,
    name STRING,
    amount DECIMAL(10,2),
    event_date DATE,
    created_at TIMESTAMP_LTZ
)
    CATALOG = 'SNOWFLAKE'
    EXTERNAL_VOLUME = 'SNOWFLAKE_MANAGED'
    ICEBERG_VERSION = 2;

INSERT INTO iceberg_v2_test VALUES
    (1, 'Alice', 100.50, '2025-01-15', CURRENT_TIMESTAMP()),
    (2, 'Bob', 250.00, '2025-02-20', CURRENT_TIMESTAMP()),
    (3, 'Charlie', 75.25, '2025-03-10', CURRENT_TIMESTAMP());

通常の Snowflake テーブルと同じように SELECT・INSERT できます。

SELECT * FROM iceberg_v2_test;
+----+---------+--------+------------+-------------------------------+
| ID | NAME    | AMOUNT | EVENT_DATE | CREATED_AT                    |
|----+---------+--------+------------+-------------------------------|
|  1 | Alice   | 100.50 | 2025-01-15 | 2026-05-21 05:25:20.941 -0700 |
|  2 | Bob     | 250.00 | 2025-02-20 | 2026-05-21 05:25:20.941 -0700 |
|  3 | Charlie |  75.25 | 2025-03-10 | 2026-05-21 05:25:20.941 -0700 |
+----+---------+--------+------------+-------------------------------+

Iceberg V3 テーブルの作成

ICEBERG_VERSION = 3を指定することで、V3 特有の機能を活用したテーブルも作成できます。ここでは、DEFAULT値の定義、VARIANT型、GEOGRAPHY型を含むテーブルを作成します。

CREATE OR REPLACE ICEBERG TABLE iceberg_v3_test (
    id INT,
    name STRING,
    metadata VARIANT,
    event_timestamp TIMESTAMP_LTZ(6),
    status STRING DEFAULT 'active',
    version INT DEFAULT 1,
    geo GEOGRAPHY
)
    CATALOG = 'SNOWFLAKE'
    EXTERNAL_VOLUME = 'SNOWFLAKE_MANAGED'
    ICEBERG_VERSION = 3;

INSERT INTO iceberg_v3_test (id, name, metadata, event_timestamp, geo)
    SELECT 1, 'Sensor-A', PARSE_JSON('{"temp": 23.5, "humidity": 65, "tags": ["indoor", "floor1"]}'),
     '2025-05-01 10:00:00'::TIMESTAMP_LTZ, ST_GEOGRAPHYFROMWKT('POINT(139.7671 35.6812)')
    UNION ALL
    SELECT 2, 'Sensor-B', PARSE_JSON('{"temp": 18.2, "humidity": 80, "tags": ["outdoor"]}'),
     '2025-05-01 10:05:00'::TIMESTAMP_LTZ, ST_GEOGRAPHYFROMWKT('POINT(135.5023 34.6937)');

-- DEFAULT 値を省略して挿入(status='active', version=1 が自動設定される)
INSERT INTO iceberg_v3_test (id, name, metadata, event_timestamp)
    SELECT 3, 'Sensor-C', PARSE_JSON('{"temp": 30.1, "humidity": 45}'),
     '2025-05-01 10:10:00'::TIMESTAMP_LTZ;

DEFAULTを指定したカラムを省略してINSERTすると、設定したデフォルト値が自動で入ることが確認できます(id = 3 のレコード)。

> SELECT * FROM iceberg_v3_test WHERE id = 3;
 +----+----------+-------------------+-------------------------------+--------+---------+----------------------------+
| ID | NAME     | METADATA          | EVENT_TIMESTAMP               | STATUS | VERSION | GEO                        |
|----+----------+-------------------+-------------------------------+--------+---------+----------------------------|
|  3 | Sensor-C | {                 | 2025-05-01 10:10:00.000 -0700 | active |       1 | NULL                       |
|    |          |   "humidity": 45, |                               |        |         |                            |
|    |          |   "temp": 30.1    |                               |        |         |                            |
|    |          | }                 |                               |        |         |                            |
|  1 | Sensor-A | {                 | 2025-05-01 10:00:00.000 -0700 | active |       1 | {                          |
|    |          |   "humidity": 65, |                               |        |         |   "coordinates": [         |
|    |          |   "tags": [       |                               |        |         |     1.397671000000000e+02, |
|    |          |     "indoor",     |                               |        |         |     3.568120000000000e+01  |
|    |          |     "floor1"      |                               |        |         |   ],                       |
|    |          |   ],              |                               |        |         |   "type": "Point"          |
|    |          |   "temp": 23.5    |                               |        |         | }                          |
|    |          | }                 |                               |        |         |                            |
|  2 | Sensor-B | {                 | 2025-05-01 10:05:00.000 -0700 | active |       1 | {                          |
|    |          |   "humidity": 80, |                               |        |         |   "coordinates": [         |
|    |          |   "tags": [       |                               |        |         |     1.355023000000000e+02, |
|    |          |     "outdoor"     |                               |        |         |     3.469370000000000e+01  |
|    |          |   ],              |                               |        |         |   ],                       |
|    |          |   "temp": 18.2    |                               |        |         |   "type": "Point"          |
|    |          | }                 |                               |        |         | }                          |
+----+----------+-------------------+-------------------------------+--------+---------+----------------------------+

https://docs.snowflake.com/en/user-guide/tables-iceberg-v3-specification-support

WRITE DEFAULT の変更

V3 では既存テーブルの列に対してWRITE DEFAULTの値を変更することもできます。変更後の WRITE DEFAULT は、以降の書き込みでその列の値が指定されなかった新規レコードに適用されさます。

https://docs.snowflake.com/en/user-guide/tables-iceberg-manage#use-default-values-with-iceberg-tables

-- WRITE DEFAULT の変更
ALTER ICEBERG TABLE iceberg_v3_test ALTER COLUMN version SET WRITE DEFAULT 2;

-- version を省略して INSERT
INSERT INTO iceberg_v3_test (id, name, metadata, event_timestamp)
    SELECT 4, 'Sensor-D', PARSE_JSON('{"temp": 22, "humidity": 55}'),
     '2025-05-02 09:00:00'::TIMESTAMP_LTZ;

-- id=4 のみ version=2 として記録される
>SELECT * FROM TEST_DB.ICEBERG_VERIFICATION.iceberg_v3_test;
+----+----------+-------------------+-------------------------------+--------+---------+----------------------------+
| ID | NAME     | METADATA          | EVENT_TIMESTAMP               | STATUS | VERSION | GEO                        |
|----+----------+-------------------+-------------------------------+--------+---------+----------------------------|
|  1 | Sensor-A | {                 | 2025-05-01 10:00:00.000 -0700 | active |       1 | {                          |
|    |          |   "humidity": 65, |                               |        |         |   "coordinates": [         |
|    |          |   "tags": [       |                               |        |         |     1.397671000000000e+02, |
|    |          |     "indoor",     |                               |        |         |     3.568120000000000e+01  |
|    |          |     "floor1"      |                               |        |         |   ],                       |
|    |          |   ],              |                               |        |         |   "type": "Point"          |
|    |          |   "temp": 23.5    |                               |        |         | }                          |
|    |          | }                 |                               |        |         |                            |
|  2 | Sensor-B | {                 | 2025-05-01 10:05:00.000 -0700 | active |       1 | {                          |
|    |          |   "humidity": 80, |                               |        |         |   "coordinates": [         |
|    |          |   "tags": [       |                               |        |         |     1.355023000000000e+02, |
|    |          |     "outdoor"     |                               |        |         |     3.469370000000000e+01  |
|    |          |   ],              |                               |        |         |   ],                       |
|    |          |   "temp": 18.2    |                               |        |         |   "type": "Point"          |
|    |          | }                 |                               |        |         | }                          |
|  3 | Sensor-C | {                 | 2025-05-01 10:10:00.000 -0700 | active |       1 | NULL                       |
|    |          |   "humidity": 45, |                               |        |         |                            |
|    |          |   "temp": 30.1    |                               |        |         |                            |
|    |          | }                 |                               |        |         |                            |
|  4 | Sensor-D | {                 | 2025-05-02 09:00:00.000 -0700 | active |       2 | NULL                       |
|    |          |   "humidity": 55, |                               |        |         |                            |
|    |          |   "temp": 22      |                               |        |         |                            |
|    |          | }                 |                               |        |         |                            |
+----+----------+-------------------+-------------------------------+--------+---------+----------------------------+

外部エンジン(DuckDB)から読み取り

Snowflake Horizon Catalog の Iceberg REST API 経由で、DuckDB からテーブルを参照してみます。

接続用ロール・ユーザー・PAT の準備

サービスユーザーを作成し、Programmatic Access Token(PAT)を発行します。

-- 読み取り用ロール
CREATE ROLE IF NOT EXISTS ICEBERG_READER_ROLE;

GRANT USAGE ON DATABASE TEST_DB TO ROLE ICEBERG_READER_ROLE;
GRANT USAGE ON SCHEMA TEST_DB.ICEBERG_VERIFICATION TO ROLE ICEBERG_READER_ROLE;
GRANT USAGE ON WAREHOUSE COMPUTE_WH TO ROLE ICEBERG_READER_ROLE;

-- ICEBERG TABLE への参照権限
GRANT SELECT ON ALL ICEBERG TABLES IN SCHEMA TEST_DB.ICEBERG_VERIFICATION
    TO ROLE ICEBERG_READER_ROLE;
GRANT SELECT ON FUTURE ICEBERG TABLES IN SCHEMA TEST_DB.ICEBERG_VERIFICATION
    TO ROLE ICEBERG_READER_ROLE;

-- サービスユーザー
CREATE OR REPLACE USER ICEBERG_DUCKDB_USER
    TYPE = SERVICE
    DEFAULT_ROLE = ICEBERG_READER_ROLE;
GRANT ROLE ICEBERG_READER_ROLE TO USER ICEBERG_DUCKDB_USER;

-- ネットワークポリシーを設定
CREATE OR REPLACE NETWORK RULE iceberg_duckdb_network_rule
    MODE = INGRESS TYPE = IPV4 VALUE_LIST = ('0.0.0.0/0'); -- 検証用。0.0.0.0/0 許可のため要注意
CREATE OR REPLACE NETWORK POLICY iceberg_duckdb_policy
    ALLOWED_NETWORK_RULE_LIST = ('iceberg_duckdb_network_rule');
ALTER USER ICEBERG_DUCKDB_USER SET NETWORK_POLICY = 'ICEBERG_DUCKDB_POLICY';

-- PAT発行
ALTER USER ICEBERG_DUCKDB_USER ADD PROGRAMMATIC ACCESS TOKEN DUCKDB_PAT
    ROLE_RESTRICTION = 'ICEBERG_READER_ROLE'
    DAYS_TO_EXPIRY = 30;

なお、ICEBERG TABLE への権限付与はON ALL TABLES IN SCHEMAの対象に含まれず、明示的にON ALL ICEBERG TABLES IN SCHEMAでの指定が必要となる点に注意します。

PAT からアクセストークンに交換

サービスユーザーの PAT を OAuth アクセストークンに交換します。

export SNOWFLAKE_ACCOUNT="<your-account>" 
export SNOWFLAKE_USER="ICEBERG_DUCKDB_USER"
export SNOWFLAKE_PAT="<上で発行したPAT>"

export ACCESS_TOKEN=$(curl -sS -X POST \
  "https://${SNOWFLAKE_ACCOUNT}.snowflakecomputing.com/oauth/token" \
  --data-urlencode "grant_type=password" \
  --data-urlencode "username=${SNOWFLAKE_USER}" \
  --data-urlencode "password=${SNOWFLAKE_PAT}")

外部エンジンから Horizon Catalog に接続する一連の手順については、公式ドキュメントにも記載があります。

https://docs.snowflake.com/en/user-guide/tables-iceberg-access-using-external-query-engine-snowflake-horizon#step-6-connect-an-external-query-engine-to-iceberg-tables-through-horizon-catalog

DuckDB からクエリ

DuckDB の iceberg 拡張から、Snowflake Horizon Catalog エンドポイント(/polaris/api/catalog)に接続します。

INSTALL iceberg;
LOAD iceberg;

CREATE OR REPLACE SECRET horizon_secret (
    TYPE ICEBERG,
    TOKEN getenv('ACCESS_TOKEN')
);

ATTACH 'TEST_DB' AS horizon (
    TYPE ICEBERG,
    ENDPOINT 'https://' || getenv('SNOWFLAKE_ACCOUNT') || '.snowflakecomputing.com/polaris/api/catalog'
);

D SHOW TABLES FROM horizon.ICEBERG_VERIFICATION;
┌─────────────────┐
name
varchar
├─────────────────┤
│ ICEBERG_V2_TEST │
│ ICEBERG_V3_TEST │
└─────────────────┘

D SELECT * FROM horizon.ICEBERG_VERIFICATION.ICEBERG_V2_TEST ORDER BY id;
┌───────┬─────────┬───────────────┬────────────┬────────────────────────────┐
│  ID   │  NAME   │    AMOUNT     │ EVENT_DATE │         CREATED_AT         │
│ int32 │ varchardecimal(10,2) │    datetimestamp with time zone
├───────┼─────────┼───────────────┼────────────┼────────────────────────────┤
1 │ Alice   │        100.502025-01-152026-05-21 21:25:20.941+09
2 │ Bob     │        250.002025-02-202026-05-21 21:25:20.941+09
3 │ Charlie │         75.252025-03-102026-05-21 21:25:20.941+09
└───────┴─────────┴───────────────┴────────────┴────────────────────────────┘

V2 テーブルは問題なく読み取れました。

一方、V3 テーブルへの SELECT は以下のエラーで失敗しました。

D SELECT * FROM horizon.ICEBERG_VERIFICATION.ICEBERG_V3_TEST ORDER BY id;
Invalid Configuration Error:
Unrecognized primitive type: geography

DuckDB の iceberg 拡張(v1.5.2)は、Iceberg V3 で追加されたgeometry/geographyなどの型が未サポートでした。SELECTで該当カラムを除外しても同じエラーになります。

https://duckdb.org/docs/current/core_extensions/iceberg/overview#limitations

V2 から存在するカラム型のみで構成された V3 テーブルであれば、DuckDB からも問題なく読み取りできました。

-- Snowflake側でV2互換のカラムのみのV3テーブルを作成
CREATE OR REPLACE ICEBERG TABLE iceberg_v3_basic (
    id INT,
    name STRING,
    amount DECIMAL(10,2),
    event_date DATE,
    created_at TIMESTAMP_LTZ
)
    CATALOG = 'SNOWFLAKE'
    EXTERNAL_VOLUME = 'SNOWFLAKE_MANAGED'
    ICEBERG_VERSION = 3;

INSERT INTO iceberg_v3_basic VALUES
    (1, 'Dave', 300.00, '2025-04-01', CURRENT_TIMESTAMP()),
    (2, 'Eve', 450.75, '2025-04-15', CURRENT_TIMESTAMP());

DuckDB から参照

D SELECT * FROM horizon.ICEBERG_VERIFICATION.ICEBERG_V3_BASIC ORDER BY id;
┌───────┬─────────┬───────────────┬────────────┬────────────────────────────┐
│  ID   │  NAME   │    AMOUNT     │ EVENT_DATE │         CREATED_AT         │
│ int32 │ varchardecimal(10,2) │    datetimestamp with time zone
├───────┼─────────┼───────────────┼────────────┼────────────────────────────┤
1 │ Dave    │        300.002025-04-012026-05-21 22:52:44.671+09
2 │ Eve     │        450.752025-04-152026-05-21 22:52:44.671+09
└───────┴─────────┴───────────────┴────────────┴────────────────────────────┘

余談:Databricks からの参照を試す

以下の記事では、カタログフェデレーションを用いて Snowflake 管理の Iceberg テーブルを Databricks から参照しています。この記事では外部ストレージを使用していますが、Snowflake ストレージを使用する場合にどのような挙動になるか確認してみました。

https://dev.classmethod.jp/articles/iceberg-delta-snowflake-snowflake-iceberg-try/

具体的には、外部ロケーションを指定せずにコネクションを作成しました。

image

参照自体は可能でしたが、テーブルのメタデータを確認するとdata_source_formatSNOWFLAKE_FORMATとなっていました。これは、このテーブルがクエリフェデレーション(Query Federation)として読み取られているためです。
Iceberg テーブルとしてオブジェクトストレージから直接読み取られているわけではなく、JDBC 経由で Snowflake にクエリが送信されているということになります。

ドキュメントによると、Databricks はカタログフェデレーション利用時、以下の順でテーブルアクセスを試みます。

  1. Iceberg テーブルとしてオブジェクトストレージから直接読み取る(カタログフェデレーション)
  2. 条件を満たさない場合、JDBC 経由で Snowflake にクエリを送信する(クエリフェデレーション)

https://docs.databricks.com/aws/en/query-federation/snowflake-catalog-federation#determine-whether-a-foreign-snowflake-table-uses-catalog-or-query-federation

SNOWFLAKE_MANAGEDの Iceberg テーブルは Databricks からはオブジェクトストレージに直接アクセスできず、フォールバックとしてクエリフェデレーション側の動作になったと考えられます。

image

さいごに

簡単ではありますが、Snowflake Storage for Apache Iceberg Tables を試してみました。外部クラウドストレージの準備が不要となり、通常の Snowflake テーブルと同じ感覚で Iceberg テーブルを利用できる点が便利と感じました。
クラウド側の設定を不要としつつも、Iceberg のオープンテーブル形式を活用したい場合に使用できる機能と思います。
こちらの内容がどなたかの参考になれば幸いです。

参考

https://www.snowflake.com/en/developers/guides/get-started-snowflake-managed-iceberg-tables/#query-from-duckdb


Snowflakeの導入支援はクラスメソッドに!

クラスメソッドでは Snowflake の導入を支援しております。
製品の詳細や支援の内容についてお気軽にお問い合わせください。

Snowflakeの詳細を見る

この記事をシェアする

関連記事