RAKUS Developers Blog | ラクス エンジニアブログ

株式会社ラクスのITエンジニアによる技術ブログです。

Ducklake(PostgresqlでカタログDB構築): Postgresql + s3(Minio) + DuckDBで構築するLakehouse

はじめに

こんにちは!
エンジニア3年目のTKDSです!
今回はメタデータ管理をPostgresqlやMySQLなどのSQLデータベースが担う新しいOSSのLakehouseフォーマット、Ducklakeについて試してみました!
今回はPostgresql+s3(minio)+DuckDBで構築してます。

Ducklakeの概要

Ducklakeは公式HPの一文によるとSQLデータベースをカタログ置き場に、データをparquetファイルに保存する形式のLakehouseフォーマットだそうです。

DuckLake delivers advanced data lake features without traditional lakehouse complexity by using Parquet files and your SQL database. It's an open, standalone format from the DuckDB team.

(Google翻訳) DuckLakeは、ParquetファイルとSQLデータベースを使用することで、従来のレイクハウスの複雑さを排除しながら、高度なデータレイク機能を提供します。DuckDBチームが提供するオープンなスタンドアロンフォーマットです。

簡単にLakehouseを使えると解釈しておきましょう!
では早速準備して使っていきます。

準備

今回、Postgresqlとminioをcomposeで建て、それぞれカタログ置き場、データ置き場として使います。
環境構築後、DuckDB CLIからクエリを打って操作していく形です 。

1. composeでPostgresqlとminioを起動

まず.envファイルにパスワード等を記載しておいてください。 Minioのパスワードは短いと以下のエラーでコンテナ起動できないので注意。

MINIO_ROOT_USER length should be at least 3, and MINIO_ROOT_PASSWORD length at least 8 characters

MINIO_ROOT_USER=root-minio
MINIO_ROOT_PASSWORD=root-minio

POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
POSTGRES_DB=postgres

ディレクトリを作っておきます。

mkdir ./data/minio
mkdir ./data/postgres

以下の内容でcomposeファイルを書きます。

services:
  minio:
    image: quay.io/minio/minio:RELEASE.2025-05-24T17-08-30Z
    container_name: minio
    command: server /data --console-address ":9001"
    environment:
      MINIO_ROOT_USER: ${MINIO_ROOT_USER}
      MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
    volumes:
      - ./data/minio:/data
    ports:
      - "127.0.0.1:9000:9000" 
      - "127.0.0.1:9001:9001" 
    healthcheck:
      test: ["CMD", "mc", "ready", "local"]
      start_period: 10s
      interval: 30s
      timeout: 20s
      retries: 3

  postgres:
    image: postgres:17.5-bookworm
    container_name: postgres
    environment:
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: ${POSTGRES_DB}
    volumes:
      - ./data/postgres/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
    ports:
      - "127.0.0.1:5432:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
      interval: 10s
      timeout: 5s
      retries: 5

networks:
  default:
    name: ducknet

init.sqlには以下を記載します。

CREATE DATABASE ducklake;

docker compose upで起動します。

2. minioでバケットを作成する

今回GUIから作りました。
http://localhost:9001/にアクセスしてください。

ログインして、左側のCreate Bucketからバケットを作ります。
今回名前はducklake-pocにしました。

3. DuckDB側の準備と起動

DuckDBをインストールします。
今回はCLIのDuckDBを使います。
HPにあるコマンド

curl https://install.duckdb.org | sh

インストールできたか確認

$ duckdb -version
v1.3.0 71c5c07cdd

DuckDBに必要な拡張機能をインストールします。
duckdbコマンドでCLIを起動して以下のコマンドを打ってください。

INSTALL ducklake;
INSTALL httpfs;
INSTALL postgres;
LOAD postgres;
LOAD ducklake;
LOAD httpfs;

postgres、httpfsはDucklakeのドキュメントには書いてなかったですが、とりあえず関係してそうなのでインストールしておきました。
Postgresqlとオブジェクトストレージに接続し、アタッチしたカタログデータベースを使用状態にします。

まずs3接続用シークレットを作ります。

create secret (type s3, key_id 'root-minio', secret 'root-minio', endpoint 'localhost:9000', use_ssl false, url_style 'path');

では接続します。

ATTACH 'ducklake:postgres:dbname=ducklake_catalog host=localhost user=postgres password=postgres' AS postgres_ducklake (DATA_PATH 's3://ducklake-poc');

USEしたときにエラーでなければ成功です。

USE postgres_ducklake;

Attachしたタイミングでテーブルが作成されます。 publicスキーマに作られてます。

$ docker exec -it postgres psql -U postgres -d ducklake
psql (17.5 (Debian 17.5-1.pgdg120+1))
Type "help" for help.

ducklake=# \dt
Did not find any relations.
ducklake=# \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
(19 rows)

ducklake=# \dt public.ducklake_column;
              List of relations
 Schema |      Name       | Type  |  Owner   
--------+-----------------+-------+----------
 public | ducklake_column | table | postgres
(1 row)

これで使う準備は完了です。

試す

Introductionのサンプルを元に試しにやってみます。 Postgresqlとminioで環境構築してるので、一部を改変して実行してます。
※色々横道それながらやってたので、一部オブジェクトファイルのファイル名違ったりしますが気にしないでください。

CREATE TABLE nl_train_stations AS
      FROM 'https://blobs.duckdb.org/nl_stations.csv';

これでCSVファイルからデータを読み込みテーブルが作成されます。

DBを見てみるとテーブルの情報が作成されてます。

ducklake=# select * from ducklake_table;
 table_id |              table_uuid              | begin_snapshot | end_snapshot | schema_id |    table_name     
----------+--------------------------------------+----------------+--------------+-----------+-------------------
        1 | 01974993-3ba1-7505-b4bf-eecff46886e0 |              1 |              |         0 | nl_train_stations
(1 row)

ducklake=# select table_id,column_name from ducklake_column;
 table_id | column_name 
----------+-------------
        1 | id
        1 | code
        1 | uic
        1 | name_short
        1 | name_medium
        1 | name_long
        1 | slug
        1 | country
        1 | type
        1 | geo_lat
        1 | geo_lng
(11 rows)

オブジェクトストレージ側にもparquetファイルが作成されます。

では続きをやっていきます。 以下コマンドはparquetファイルの情報を取得しています。

D FROM glob('s3://ducklake-poc/*') ;
┌─────────────────────────────────────────────────────────────────────────┐
│                                  file                                   │
│                                 varchar                                 │
├─────────────────────────────────────────────────────────────────────────┤
│ s3://ducklake-poc/ducklake-01974989-7208-7460-964d-c17d34b116c7.parquet │
└─────────────────────────────────────────────────────────────────────────┘
D FROM 's3://ducklake-poc/*.parquet' LIMIT 10;
┌───────┬─────────┬───┬─────────────────┬─────────────────┐
│  id   │  code   │ … │     geo_lat     │     geo_lng     │
│ int64 │ varchar │   │     double      │     double      │
├───────┼─────────┼───┼─────────────────┼─────────────────┤
│   266 │ HT      │ … │        51.69048 │         5.29362 │
│   269 │ HTO     │ … │ 51.700553894043 │ 5.3183331489563 │
│   227 │ HDE     │ … │      52.4091682 │        5.893611 │
│     8 │ AHBF    │ … │         50.7678 │        6.091499 │
│   818 │ AW      │ … │        50.78036 │        6.070715 │
│    51 │ ATN     │ … │ 51.921326524551 │ 6.5786272287369 │
│     5 │ AC      │ … │         52.2785 │           4.977 │
│   550 │ EAHS    │ … │ 52.079796120944 │ 7.0163583755493 │
│    12 │ AIME    │ … │        45.55438 │         6.64869 │
│   819 │ ACDG    │ … │       49.004048 │        2.571133 │
├───────┴─────────┴───┴─────────────────┴─────────────────┤
│ 10 rows                            11 columns (4 shown) │
└────────────────

チュートリアルの通りに変更を加えます。

UPDATE nl_train_stations SET name_long='Johan Cruijff ArenA' WHERE code = 'ASB';

変更を加えるとファイルが増えてるのがわかります。

FROM glob('s3://ducklake-poc/*') ;
┌──────────────────────────────────────────────────────────────────────────────┐
│                                     file                                     │
│                                   varchar                                    │
├──────────────────────────────────────────────────────────────────────────────┤
│ s3://ducklake-poc/ducklake-01974993-3ba1-7387-ac08-101cab81f9f7.parquet      │
│ s3://ducklake-poc/ducklake-01974994-9e69-7177-a6e5-253d9cf9d4af.parquet      │
│ s3://ducklake-poc/ducklake-01974994-9e83-7c25-b00b-1f41f679902e-delete.par…  │
└──────────────────────────────

過去のデータの状態を記憶したsnapshot一覧も見ることができるようです。

FROM postgres_ducklake.snapshots();
┌─────────────┬────────────────────────────┬────────────────┬─────────────────────────────────────────────────────────────────────┐
│ snapshot_id │       snapshot_time        │ schema_version │                               changes                               │
│    int64    │  timestamp with time zone  │     int64      │                       map(varchar, varchar[])                       │
├─────────────┼────────────────────────────┼────────────────┼─────────────────────────────────────────────────────────────────────┤
│           0 │ 2025-06-07 17:43:15.138+09 │              0 │ {schemas_created=[main]}                                            │
│           1 │ 2025-06-07 17:47:54.805+09 │              1 │ {tables_created=[main.nl_train_stations], tables_inserted_into=[1]} │
│           2 │ 2025-06-07 17:49:26.108+09 │              1 │ {tables_inserted_into=[1], tables_deleted_from=[1]}     

バージョンごとに内容を確認可能です。

SELECT name_long FROM nl_train_stations AT (VERSION => 1) WHERE code = 'ASB';
nl_train_stations AT (VERSION => 2) WHERE code = '┌─────────────────────────┐
│        name_long        │
│         varchar         │
├─────────────────────────┤
│ Amsterdam Bijlmer ArenA │
└─────────────────────────┘
 SELECT name_long FROM nl_train_stations AT (VERSION => 2) WHERE code = 'ASB';
┌─────────────────────┐
│      name_long      │
│       varchar       │
├─────────────────────┤
│ Johan Cruijff ArenA │
└──────────────

カタログからデタッチするには以下のコマンドを使います。

USE memory;
DETACH postgres_ducklake;

ここまでで色々試すことができました!

まとめ

今回はDucklakeをPostgresql + s3(Minio) + DuckDBで構築しました!
インターネット上に情報が少なくかなり苦労しましたが、動かせてよかったです!
データ本体はオブジェクトストレージに置く形式なので、低コストで大量のデータをおけるのではないでしょうか?
これからもちょくちょく触ってみたいと思いました。
ここまで読んでいただきありがとうございました!

Copyright © RAKUS Co., Ltd. All rights reserved.