Snowflake ストレージを使用する Iceberg テーブルの DuckDB からの参照を試してみる
はじめに
2026年4月のアップデートで、Apache Iceberg テーブル用の Snowflake ストレージがパブリックプレビューとなりました。
外部のクエリエンジン(DuckDB)からの参照を試してみた内容を本記事でまとめます。
アップデートの概要
本機能については以下に記載があります。
これまで Iceberg テーブルを作成するには、外部クラウドストレージ(S3 など)を準備し、外部ボリュームとして登録する必要がありました。本機能により、ストレージの構成・運用を Snowflake に委ねた上で Iceberg テーブルを利用できるようになります。
具体的には、外部クラウドストレージの準備や外部ボリュームの登録が不要でEXTERNAL_VOLUME = 'SNOWFLAKE_MANAGED'を指定するだけで Iceberg テーブルを作成できます。永続的な Iceberg テーブルは Snowflake のフェイルセーフによる保護対象にもなります。
Snowflake をカタログとして使用
Snowflake をカタログとして使用するので、Snowflake 側がメタデータのライフサイクルを管理し、テーブルデータとスナップショットの保持期間に基づいて、古いメタデータ・マニフェストリスト・マニフェストファイルが自動的に削除されます。外部カタログを使う場合に必要となるメタデータ管理の運用負荷を軽減できます。
外部エンジンからのアクセス
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 経由で外部クエリエンジンからアクセスできます。
試してみる
前提条件
本記事では以下の環境を使用しています。
- 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" |
| | | } | | | | } |
+----+----------+-------------------+-------------------------------+--------+---------+----------------------------+
WRITE DEFAULT の変更
V3 では既存テーブルの列に対してWRITE DEFAULTの値を変更することもできます。変更後の WRITE DEFAULT は、以降の書き込みでその列の値が指定されなかった新規レコードに適用されさます。
-- 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 に接続する一連の手順については、公式ドキュメントにも記載があります。
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 │ varchar │ decimal(10,2) │ date │ timestamp with time zone │
├───────┼─────────┼───────────────┼────────────┼────────────────────────────┤
│ 1 │ Alice │ 100.50 │ 2025-01-15 │ 2026-05-21 21:25:20.941+09 │
│ 2 │ Bob │ 250.00 │ 2025-02-20 │ 2026-05-21 21:25:20.941+09 │
│ 3 │ Charlie │ 75.25 │ 2025-03-10 │ 2026-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で該当カラムを除外しても同じエラーになります。
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 │ varchar │ decimal(10,2) │ date │ timestamp with time zone │
├───────┼─────────┼───────────────┼────────────┼────────────────────────────┤
│ 1 │ Dave │ 300.00 │ 2025-04-01 │ 2026-05-21 22:52:44.671+09 │
│ 2 │ Eve │ 450.75 │ 2025-04-15 │ 2026-05-21 22:52:44.671+09 │
└───────┴─────────┴───────────────┴────────────┴────────────────────────────┘
余談:Databricks からの参照を試す
以下の記事では、カタログフェデレーションを用いて Snowflake 管理の Iceberg テーブルを Databricks から参照しています。この記事では外部ストレージを使用していますが、Snowflake ストレージを使用する場合にどのような挙動になるか確認してみました。
具体的には、外部ロケーションを指定せずにコネクションを作成しました。

参照自体は可能でしたが、テーブルのメタデータを確認するとdata_source_formatがSNOWFLAKE_FORMATとなっていました。これは、このテーブルがクエリフェデレーション(Query Federation)として読み取られているためです。
Iceberg テーブルとしてオブジェクトストレージから直接読み取られているわけではなく、JDBC 経由で Snowflake にクエリが送信されているということになります。
ドキュメントによると、Databricks はカタログフェデレーション利用時、以下の順でテーブルアクセスを試みます。
- Iceberg テーブルとしてオブジェクトストレージから直接読み取る(カタログフェデレーション)
- 条件を満たさない場合、JDBC 経由で Snowflake にクエリを送信する(クエリフェデレーション)
SNOWFLAKE_MANAGEDの Iceberg テーブルは Databricks からはオブジェクトストレージに直接アクセスできず、フォールバックとしてクエリフェデレーション側の動作になったと考えられます。

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








