pg_duckdbとDuckLakeがもたらすOLAP統合の未来
注目を集めるPostgreSQL+Analytics
先日、SnowflakeとDatabricksのそれぞれの年次イベントでPostgreSQLに関連する企業の買収が大々的に発表されました。
両社は分析系(OLAP)のソリューションを提供する比較的大きなベンダーであり、過去にはOLTP系への進出を目指したデータストアの開発が注目されたこともありました(SnowflakeのUnistoreが典型です)。
彼らは今後、PostgreSQLを自社がカバーできていなかった領域で適用することで、現在のメガクラウドのようにOLTP用途のRDBとOLAPのソリューションを統合してくることが予想されます。
そして、多くの利用者を持つオープンソースのPostgreSQL(コミュニティ版と言っても良いかも知れません)においても、OLAPとの統合という流れは今後確実に訪れるというのが、私個人の予想です。
今回はその先駆けとして注目している、PostgreSQLとDuckDBの統合について、実際に試しながら将来の姿を予想していきます。
PostgreSQLとOLAP
DuckDBの話に入る前に、PostgreSQLでOLAPソリューションが実現されてきた歴史を簡単に振り返っておきます。
PostgreSQLでは分析系の機能を強化するために、カラムナストア(分析処理に適した列形式のデータ構造)を追加実装するCitusやGoogle AlloyDBのようなアプローチがありました(こうした流れは以前のスライドで説明しています)。
しかし、このアプローチではカラムナストアをPostgreSQLの内部に保持することになり、データレイクなどに蓄積された列バイナリファイルを読み込むにはETLツールなどでロードする必要がありました。
こうしたETLの手間を省き、構築済みのデータレイク上のファイル(IcebergやParquet等)にPostgreSQLから直接アプローチは出来ないのでしょうか?
これを部分的に実現しているのがpg_duckdbであり、最近発表されたDuckLakeとの連携が強化されれば、完成形に近づく可能性があるのではないかと期待しています。
pg_duckdbとは
pg_duckdbはPostgreSQLの拡張モジュールであり、組み込みの分析エンジンであるDuckDBの機能をPostgreSQLに統合し、OLTPだけでなくOLAPを並行して高速に処理可能となるデータベース(いわゆるHTAP)を実現することを目指しています。
通常のDuckDBのユースケースでは、クライアントPC上や仮想マシンでシングルプロセスとして起動され、ローカルまたはオブジェクトストレージ上のデータファイルにアクセスしてSQLによる分析処理を行います。
一方でpg_duckdbでは、DuckDBはPostgreSQLに統合されており、psqlなどのクライアントからSQLを実行して、分析用のデータファイルにアクセスします。この際のデータファイルはPostgreSQLが内部で管理するものではなく、データレイクとしてオブジェクトストレージ上に配置されたものとなります。
pg_duckdbのアーキテクチャを簡単に表すと、下図のようになります。
「pg_duckdbのアーキテクチャ」
試しに使ってみる
pg_duckdbを試す際には、dockerでの利用が簡単です。
Readmeにあるコマンドを少々手直しして、以下のようにpg_duckdbが導入された最新のPostgreSQL(この場合はv17)を起動することが出来ます[1]。
$ docker run -d -p 5432:5432 -e POSTGRES_PASSWORD=duckdb pgduckdb/pgduckdb:17-main
これだけでpg_duckdbは利用可能ですが、PostgreSQLからオブジェクトストレージを参照するためにはSecretを設定する必要があります。ここではGoogle Cloud Storage(GCS)を利用する場合を例にしています(keyとsecretは適宜変更してください)。
$ SELECT duckdb.create_simple_secret(
type := 'GCS',
key_id := 'key',
secret := 'secret'
);
これで準備が整いました。
大まかに言えば、pg_duckdbでは以下の処理をPostgreSQLから実行できます。[2]
- S3やGCSなどのオブジェクトストレージへParquetファイルを書き込む
- オブジェクトストレージ上のファイル(CSV/Parquet/JSON/Iceberg/Delta)を読み込む
1. Parquetファイルを書き込む
こちらは非常に簡単です。事前に用意したitems
テーブルの内容を全てGCSに書き出すには、以下のSQLを実行します。
$ COPY (SELECT * FROM items) TO 'gs://pg-duckdb-test/items.parquet';
上記SQLでGCSのpg-duckdb-testバケットにitems.parquetというファイルが作成されます。
実際のユースケースでは、より複雑なSQLでデータを加工したり他のテーブルと結合したりしながらファイルを生成することで、これまでETLツールが担っていたTransformを一部省略することが期待されます。
2. Parquetファイルを読み込む
pg_duckdbでは様々な関数が用意されており、DuckDBの拡張と組み合わせて色々な形式のファイルを読み込むことが出来ます。ここでは列形式のバイナリファイルとして良く利用されるParquetファイルを読み込んでみましょう。
読み込み対象として、先ほど作成したitems.parquetを指定します(処理としては無意味ですがお試しで)。
$ SELECT * FROM read_parquet('gs://pg-duckdb-test/items.parquet');
id | name | price
--------------------------------------+------+-------
ed10117c-e274-4be1-a6f2-d57e40c1f781 | AAA | 500
df8d20c4-8b57-4abd-95bf-825d4cf2fea8 | BBB | 200
a5f4a014-cb03-4b14-b299-e2771eb6beff | CCC | 1000
5b0f8187-3e8f-46e6-a682-8e91fdeeca66 | DDD | 2000
e511fdbf-6c48-454b-823e-1ef04ce704fa | EEE | 5000
こちらも実際のユースケースでは、Parquetの内容を集計したりPostgreSQL上にある通常のテーブルと結合したりしながら、Load無しで高速な分析クエリを実行することが期待されます。
pg_duckdbのまとめ
なお、DuckDBは比較的小さなデータセット(~300GB程度)を分析することには優れているが、それ以上の大規模なデータセットを豊富なCPUリソースを利用して分析する際にスケールしないとした調査結果もあります。[3]
こうした事を踏まえ、ETL処理の実装などを省略して数百GB程度のデータを対象にPostgreSQLから高速な分析を行う用途として、pg_duckdbは期待できるソリューションであると考えています。
DuckLakeとは
さて、ここからはDuckLakeの話に移ります。
私のようなRDBを普段使っているエンジニアにとって、DuckLakeは理解が難しく、それが生まれてきた背景を理解しないと意義がわからないものでした。その説明にはIcebergやDelta LakeといったOpen Table Format(OTF)が実現された歴史から語る必要があると思いますが、それは専門家に譲ることとしましょう。[4]
今回は、pg_duckdbに将来的に必要なはずなのに実現されていない機能を整理し、そこをDuckLakeがどう補完するのかという観点で見ていきます。
pg_duckdbの課題
先ほどpg_duckdbを利用して、Parquetファイルを読み込みました。しかし、先の例では1ファイル=1テーブルになっていることに気付いたでしょうか。
また、書き込みにおいてもParquetファイル全体を一括して書き出しており、RDBであればできるはずのレコード単位の更新や削除が実現できていません。
「pg_duckdbの課題」
簡単な要件であればこれでも可能ですが、Icebergなどのフォーマットで複数のデータファイルをテーブルとして管理し、更新履歴を認識しながら最新やスナップショットのデータにアクセスする事が、現状のpg_duckdbでは出来ません。
DuckDBおよびpg_duckdbにはIceberg拡張があり、それらを使うことでオブジェクトストレージ上に(データファイルとは別に)格納されたマニフェストファイルやメタデータを合わせて読み取り、テーブルとして認識することは一部可能ではあります。
それでもスキーマやデータのバージョンなどのメタデータをオブジェクトストレージ上に素朴に保存するという構造は、RDBを長く使っているエンジニア程、違和感を覚えるのではないでしょうか。
Parquetなどのデータファイルがオブジェクトストレージにあるのは理解できますが、そのメタデータはRDBで管理しても良いはずで、それが実現すれば、PostgreSQLから見たストレージエンジンとしての機能分離が明快になります。
このように、「pg_duckdbをPostgreSQLのストレージエンジンとして完成させるパーツ」がDuckLakeになると言うのが、私個人の見方です。
DuckLake+PostgreSQL
DuckLakeの構成を理解するには、以下の3つのコンポーネントの理解が必要です。
- DuckDB(SQLエンジン)
- PostgreSQL(カタログデータベース)
- オブジェクトストレージ(データファイルの格納先)
先ほどのpg_duckdbの構成ではSQLエンジンをPostgreSQLとその拡張が担当し、データファイルはオブジェクトストレージに格納されていました。そして、Parquetファイルを単純に扱うのみで、カタログデータベースは存在しませんでした。
しかし、下図のDuckLakeのアーキテクチャでは、SQLエンジンはDuckDBが担当します。データファイルはpg_duckdbの構成と同様にオブジェクトストレージに格納されますが、「どんなテーブルの、どのバージョンのデータが、どのデータファイルとして保存されているのか」といった情報(カタログデータベース)はPostgreSQLに格納されます。
「DuckLake(with PostgreSQL)のアーキテクチャ」
なお、今回はDuckLakeのカタログデータベースをPostgreSQLに格納していますが、ドキュメントには以下の記述があり、MySQLなどの他のDBも利用可能です。
・Catalog database: DuckLake requires a database that supports transactions and primary key constraints as defined by the SQL-92 standard.
試しに使ってみる
では、DuckLakeも少し試してみましょう。
まず、SQLエンジンとしてのDuckDBをインストールする必要があります。公式のインストールページからプラットフォームを選択して、必要なファイルをダウンロードします。
今回はWindowsでコマンドラインツールとしてDuckDBを使っていきます。
以下のサンプルで、D
で始まるものがDuckDB内で実行しているコマンドとなります。
$ ./duckdb
D install ducklake;
D install postgres;
D CREATE SECRET (TYPE gcs, KEY_ID 'key', SECRET 'secret');
D ATTACH 'ducklake:postgres:dbname=ducklake_pg host=localhost user=postgres password=duckdb' AS ducklake_pg (DATA_PATH 'gs://pg-duckdb-test');
D USE ducklake_pg;
ここではまず、DuckDBを起動してDuckLakeとPostgreSQL用の拡張をインストールしています。
その後、GCSの認証情報を設定しています。これはpg_duckdbのお試しの際と同じバケットのものとなります。
そして、ATTACHコマンドでDuckDB上にducklake_pg
というデータベースを作成しています。
ATTACHコマンドではカタログデータベースの格納先として、先ほどpg_duckdbのお試しで利用したPostgreSQLを指定し、データファイルの格納先としてGCSのpg-duckdb-test
バケットを指定しています(なお、事前にducklake_pgのデータベースをPostgreSQL内に作成しておく必要があります。)
ここまで準備が完了し、DuckLakeでpg-duckdb-test
のバケットからファイルを読み込み、書き込むことが可能になりました。
1. DuckLakeファイルの書き込み
まずは書き込み処理として、オープンデータ(東京都公金を納付できる金融機関一覧)を読み込んで、DuckLakeにテーブルを作成します。
DuckDBではread_csv_auto関数でencodingを指定することで、Shift_JISなどのファイルも読み込むことが出来ます。[5]それを利用して、CREATE TABLE AS SELECTでテーブルを作成しています。
また、pg_duckdbの場合と異なり、DuckLakeではレコード単位の更新や削除が可能ですので、UPDATE文も以下で実行しています。
D CREATE TABLE tokyo_koukinshunou AS SELECT * FROM read_csv_auto('https://www.opendata.metro.tokyo.lg.jp/kaikeikanri/koukinshuunou.csv',encoding='Shift_JIS');
D UPDATE tokyo_koukinshunou SET 備考='text' WHERE コード=1;
2. DuckLakeファイルの読み込み
では、1.で書き込んだファイルをDuckDBから読み込んでみましょう。
D FROM tokyo_koukinshunou;
┌────────────────────────┬────────┬──────────────────────────────┬──────────────┬──────────────────────────────────────┐
│ 区分 │ コード │ 金融機関名 │ ペイジー対応 │ 備考 │
│ varchar │ int64 │ varchar │ varchar │ varchar │
├────────────────────────┼────────┼──────────────────────────────┼──────────────┼──────────────────────────────────────┤
│ 都市銀行 │ 9 │ 三井住友銀行 │ * │ NULL │
│ 都市銀行 │ 5 │ 三菱UFJ銀行 │ * │ NULL │
│ 都市銀行 │ 10 │ りそな銀行 │ * │ NULL │
(途中略)
│ 都市銀行 │ 1 │ みずほ銀行 │ * │ text │
├────────────────────────┴────────┴──────────────────────────────┴──────────────┴──────────────────────────────────────┤
│ 170 rows (40 shown) 5 columns │
└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
ここでSELECT文がPostgreSQLなどとは違う構文になっていることに気付いたでしょうか。
DuckDBではFROM-First Syntaxという構文があり、SELECT文をFROM句から書き始めて以降を省略することができます。
このSyntaxにおいては、
FROM tokyo_koukinshunou;
と、
SELECT * FROM tokyo_koukinshunou;
は同じ意味になっています。
ここまででDuckLakeへの書き込みと読み込みは出来ました。
しかし、内部では何が起こっているのでしょうか。それを確認するために、PostgreSQL内のカタログデータベース、そしてオブジェクトストレージ上のファイルを実際に覗いてみることにしましょう。
DuckLakeの仕組み
DuckLakeの仕組みを理解するために、カタログデータベースにどのようなテーブルが作られているのかを確認しましょう。
PostgreSQLのデータベースdatalake_pg
でテーブルの一覧を出力すると、以下のようになっています。
$ \dt
List of relations
Schema | Name | Type | Owner
--------+---------------------------------------+-------+----------
public | ducklake_column | table | postgres
public | ducklake_column_tag | table | postgres
public | ducklake_data_file | table | postgres
public | ducklake_delete_file | table | postgres
public | ducklake_file_column_statistics | table | postgres
public | ducklake_file_partition_value | table | postgres
public | ducklake_files_scheduled_for_deletion | table | postgres
public | ducklake_inlined_data_tables | table | postgres
public | ducklake_metadata | table | postgres
public | ducklake_partition_column | table | postgres
public | ducklake_partition_info | table | postgres
public | ducklake_schema | table | postgres
public | ducklake_snapshot | table | postgres
public | ducklake_snapshot_changes | table | postgres
public | ducklake_table | table | postgres
public | ducklake_table_column_stats | table | postgres
public | ducklake_table_stats | table | postgres
public | ducklake_tag | table | postgres
public | ducklake_view | table | postgres
ducklake_table
はテーブル名などの情報を、ducklake_data_file
はオブジェクトストレージ上のデータファイルのパスを管理していると予想されますし、ducklake_snapshot
はテーブルのスナップショットを管理し、ducklake_delete_file
は削除履歴などを管理していると推測することができます。
実際に中を見ると、以下のようになっています。
$ SELECT * FROM ducklake_table;
table_id | table_uuid | begin_snapshot | end_snapshot | schema_id | table_name
----------+--------------------------------------+----------------+--------------+-----------+--------------------
2 | 019783ad-5c6e-708c-a0d2-1a30035bfd4a | 2 | | 0 | tokyo_koukinshunou
$ SELECT * FROM ducklake_snapshot;
snapshot_id | snapshot_time | schema_version | next_catalog_id | next_file_id
-------------+----------------------------+----------------+-----------------+--------------
0 | 2025-06-18 14:40:04.489+00 | 0 | 1 | 0
1 | 2025-06-18 15:05:21.133+00 | 1 | 2 | 1
2 | 2025-06-18 15:34:25.895+00 | 2 | 3 | 2
3 | 2025-06-18 15:40:08.262+00 | 2 | 3 | 4
$ SELECT data_file_id,table_id,path,file_format FROM ducklake_data_file;
data_file_id | table_id | path | file_format
--------------+----------+-------------------------------------------------------+-------------
1 | 2 | ducklake-019783ad-5c69-7c6b-a1cd-745d8b8d799e.parquet | parquet
2 | 2 | ducklake-019783b2-9579-796d-9e83-dae3b0d707b9.parquet | parquet
$ SELECT delete_file_id,table_id,path,format FROM ducklake_delete_file;
delete_file_id | table_id | path | format
----------------+----------+--------------------------------------------------------------+---------
3 | 2 | ducklake-019783b2-9fa4-71f9-83a9-b435ca2ab35b-delete.parquet | parquet
そして、GCSのバケットを覗いてみると、ducklake_data_file
やducklake_delete_file
のpathにParquetファイルが格納されています。
このようにDuckLakeを用いると、オブジェクトストレージ上の複数ファイルをテーブルとして管理しながら、最新版のデータを読み取ったり、レコード単位で更新や削除を行うことが可能になります。
pg_duckdbとDuckLakeは現時点で連携不可
さて、ここまででDuckLake自体も動かして仕組みを理解することはできました。
しかし、DuckLakeのSQLエンジンがDuckDBであり、pg_duckdbでなかった点を不思議には思わないでしょうか?
pg_duckdbがDuckLakeフォーマットに対応してカタログデータベースを参照するようになれば、PostgreSQLをインターフェースとしたワンストップな分析ツールセットとして機能します。
しかし、2025年6月時点でpg_duckdbはDuckLakeに対応していません。
但し、pg_duckdbのIssueとして、pg_ducklakeという統合が提案されており、これが実現すると下図のようなアーキテクチャになると想定されます。
pg_duckdb+DuckLakeで想定されるアーキテクチャ
まとめ
今回はPostgreSQLでOLAP統合を実現する可能性のあるpg_duckdb、およびそれを支えるであろうDuckLakeについて紹介しました。
pg_duckdb+DuckLakeの仕組みとして提示した図を改めて眺めると、これはデータファイルをオブジェクトストレージへ格納する新たなストレージエンジンそのものです。
こうしたPostgreSQLの多機能化について、OLAP以外にも興味深いものがあれば、今後も紹介をしていきたいと思います。
-
pg_duckdbを自身でビルドして、利用中のPostgreSQLに拡張として組み込むことも出来ます。 ↩︎
-
DuckDBのマネージドサービスであるMotherDuckとの連携機能などもありますが、今回は省略します。 ↩︎
-
"The DuckLake Manifesto"として、彼らから見たOTF関連の歴史や問題意識などがまとめられています。 ↩︎
-
なお、今回ケースではカラム名が日本語になってしまっていますが、おそらく英数字のみで別途定義した方が良いです。 ↩︎

NTT DATA公式アカウントです。 技術を愛するNTT DATAの技術者が、気軽に楽しく発信していきます。 当社のサービスなどについてのお問い合わせは、 お問い合わせフォーム nttdata.com/jp/ja/contact-us/ へお願いします。
Discussion