概要
~/duckdb-docker/ のDuckDB Docker環境について、セットアップ〜利用方法〜MySQLとの対比までをまとめた。README自体をこの内容に全面改訂している(リポジトリ: hit1023/duckdb-docker)。ここに載っているコマンド・SQL・実行結果はすべてMac miniの実環境で検証済み。
構成
-
DuckDB Web UI: ブラウザからSQLを実行・可視化(
duckdb --uiをコンテナ内で起動) -
nginx: IPv6/IPv4プロキシ(DuckDB UIのlocalhost問題を解決。コンテナ内で
[::1]:4213にリッスンしているDuckDB UIを80番にプロキシする)
コンテナはUbuntu 24.04ベースで、起動時にDuckDB CLI(最新版)とui拡張をインストールする。
セットアップ方法
前提条件
- Docker / Docker Composeが使えること
- ホスト側でSQLファイルを流し込む場合はローカルにファイルがあればOK。DuckDB CLI自体をホストにインストールする必要はない
初回セットアップ
cd ~/duckdb-docker
# イメージをビルドして起動(初回はDuckDB CLIのダウンロード等で数分かかる)
docker compose up -d --build
# または: ./run.sh up
起動後、ブラウザで http://localhost:4213 を開くとDuckDB Web UIが表示される。
停止する場合:
docker compose down
# または: ./run.sh down
ポート
| ポート | 用途 |
|---|---|
| 4213 | DuckDB Web UI(nginx経由、ホストからはこちらを使う) |
| 4214 | DuckDB Web UIへの直接アクセス(コンテナ内nginxを経由しない。切り分け用) |
ディレクトリ構成
.
├── docker-compose.yml # Docker設定(ポート、ボリュームマウント)
├── Dockerfile # DuckDB CLI + nginx をインストールするイメージ定義
├── entrypoint.sh # コンテナ起動時にnginxとDuckDB UIを立ち上げるスクリプト
├── nginx.conf # nginxプロキシ設定
├── run.sh # 操作用ラッパースクリプト(後述)
├── sample.sql # 動作確認用のサンプルSQL
├── data/ # DBファイル・エクスポートデータ(.gitignore済み、コンテナ内 /db にマウント)
│ └── notebooks/ # Web UIのノートブック・設定(ui.db、.gitignore済み)
└── queries/ # 自分で書いたSQLファイルの置き場
data/はコンテナの/dbにマウントされているため、Web UI・CLIどちらからも同じファイルとして見える。
利用方法
1. Web UIから使う
-
docker compose up -d(または./run.sh up)で起動 - ブラウザで http://localhost:4213 を開く
- ノートブック内でDBファイルにアタッチしてSQLを実行
-- data/ 以下のDBファイルにアタッチ(コンテナ内パスは /db 配下)
ATTACH '/db/mydb.duckdb' AS mydb;
USE mydb;
2. コンテナ内のDuckDB CLIを直接使う
# コンテナ内でシェルを取得してCLIを起動
docker compose exec duckdb duckdb /db/mydb.duckdb
# ワンライナーでクエリを実行
docker compose exec duckdb duckdb /db/mydb.duckdb -c "SELECT * FROM users;"
# SQLファイルをホストから流し込む(-Tで標準入力をリダイレクト)
docker compose exec -T duckdb duckdb /db/mydb.duckdb < sample.sql
docker compose exec -T duckdb duckdb /db/mydb.duckdb < queries/foo.sql
Web UI(duckdb --ui)とCLI(duckdb)は同じDBファイルを同時に書き込みモードで開けない。Web UI起動中にCLIから書き込みたい場合は-readonlyを付けるか、一旦Web UI側のアタッチを解除する。
3. run.sh を使う(推奨)
docker composeコマンドをまとめたラッパースクリプトを新規作成した。リポジトリ直下から実行する。
./run.sh up # ビルドしてバックグラウンド起動
./run.sh down # コンテナ停止・削除
./run.sh restart # 再起動
./run.sh logs # ログをフォロー表示
./run.sh shell # コンテナ内でDuckDB CLIを起動(./data/mydb.duckdbにアタッチ)
./run.sh sql sample.sql # SQLファイルを ./data/mydb.duckdb に対して実行
./run.sh sql queries/foo.sql mydb2.duckdb # DBファイルを指定して実行
4. クライアントアプリから使う
Harlequin(TUI)でも接続可能:
brew install harlequin
# 読み取り専用(Web UI起動中でも接続可)
harlequin -r ./data/mydb.duckdb
# 読み書き(Web UI・CLIが同DBを開いていないこと)
harlequin ./data/mydb.duckdb
対応フォーマット
標準でjsonとparquet拡張が入っており、追加インストールなしで以下がすぐ使える。
SELECT * FROM read_csv_auto('/db/users.csv');
SELECT * FROM read_parquet('/db/users.parquet');
SELECT * FROM read_json_auto('/db/users.json');
-- globパターンで複数ファイルをまとめて読み込みも可能
SELECT * FROM read_parquet('/db/*.parquet');
それ以外の形式は拡張機能を追加インストールすれば使える(コンテナはインターネットに出られるのでINSTALLは可能。イメージには焼き込まれていないため、コンテナを再作成すると再インストールが必要)。
| 拡張 | 用途 | 有効化コマンド |
|---|---|---|
httpfs |
S3 / HTTP(S) / GCSなど、リモートのCSV・Parquetを直接クエリ | INSTALL httpfs; LOAD httpfs; |
sqlite_scanner |
SQLiteファイルをそのままATTACH | INSTALL sqlite; LOAD sqlite; |
postgres_scanner |
PostgreSQLに直接ATTACHしてクエリ | INSTALL postgres; LOAD postgres; |
mysql_scanner |
MySQLに直接ATTACH | INSTALL mysql; LOAD mysql; |
spatial |
Shapefile/GeoJSON等の地理空間データ、ST_Read経由でExcel(.xlsx)読み込みも可 |
INSTALL spatial; LOAD spatial; |
iceberg |
Apache Icebergテーブルの読み込み | INSTALL iceberg; LOAD iceberg; |
delta |
Delta Lakeテーブルの読み込み | INSTALL delta; LOAD delta; |
avro |
Avroファイルの読み込み | INSTALL avro; LOAD avro; |
Parquetとは(補足)
Parquetは CSV/JSON のような行指向のテキスト形式ではなく、Apache製の列指向(columnar)バイナリ形式。カラムごとにまとめて圧縮して保持するため、catやxxdでそのまま人間が読める内容にはならない。
- 列指向: 行ではなく列単位でデータをまとめて格納するので、特定の列だけを読む集計クエリが高速・省メモリ
- バイナリ+圧縮: SNAPPY/GZIP等で列ごとに圧縮されるため、同じデータでもCSV/JSONよりファイルサイズが小さくなりやすい
- スキーマ内蔵: 列名・型がファイル自体に埋め込まれている
-
フッターにメタデータ: ファイル末尾にスキーマや統計情報を持つフッターがあり、先頭・末尾に
PAR1というマジックナンバーが付く
実際に3つのファイルを比べると、同じ内容でもサイズが異なる(小さいデータではメタデータ分むしろParquetの方が大きくなる):
$ ls -la data/users.csv data/users.json data/users.parquet
-rw-r--r-- 57 data/users.csv
-rw-r--r-- 113 data/users.json
-rw-r--r-- 718 data/users.parquet
バイナリであることはxxdで確認できる(先頭と末尾にPAR1マジックナンバー):
$ xxd data/users.parquet | head -1
00000000: 5041 5231 1504 1510 1514 4c15 0415 0000 PAR1......L.....
$ xxd data/users.parquet | tail -1
000002c0: 6432 6636 2900 6a01 0000 5041 5231 d2f6).j...PAR1
Parquetの例
data/users.parquet(id, name, cityの2行)を実際にクエリすると:
SELECT * FROM read_parquet('/db/users.parquet');
┌───────┬──────────┬─────────┐
│ id │ name │ city │
│ int32 │ varchar │ varchar │
├───────┼──────────┼─────────┤
│ 1 │ 田中太郎 │ 横浜 │
│ 2 │ 鈴木花子 │ 東京 │
└───────┴──────────┴─────────┘
CSVから作る場合はCOPYで変換できる:
COPY (SELECT * FROM read_csv_auto('/db/users.csv')) TO '/db/users.parquet' (FORMAT parquet);
内部のスキーマ・列ごとの圧縮方式を覗ける関数もある:
-- スキーマ構造
SELECT name, type, num_children FROM parquet_schema('/db/users.parquet');
┌───────────────┬────────────┬──────────────┐
│ name │ type │ num_children │
│ varchar │ varchar │ int64 │
├───────────────┼────────────┼──────────────┤
│ duckdb_schema │ NULL │ 3 │
│ id │ INT32 │ NULL │
│ name │ BYTE_ARRAY │ NULL │
│ city │ BYTE_ARRAY │ NULL │
└───────────────┴────────────┴──────────────┘
-- 列ごとの圧縮方式・サイズ
SELECT column_id, path_in_schema, num_values, compression,
total_compressed_size, total_uncompressed_size
FROM parquet_metadata('/db/users.parquet');
┌───────────┬────────────────┬────────────┬─────────────┬───────────────────────┬─────────────────────────┐
│ column_id │ path_in_schema │ num_values │ compression │ total_compressed_size │ total_uncompressed_size │
│ int64 │ varchar │ int64 │ varchar │ int64 │ int64 │
├───────────┼────────────────┼────────────┼─────────────┼───────────────────────┼─────────────────────────┤
│ 0 │ id │ 2 │ SNAPPY │ 56 │ 111 │
│ 1 │ name │ 2 │ SNAPPY │ 79 │ 135 │
│ 2 │ city │ 2 │ SNAPPY │ 68 │ 123 │
└───────────┴────────────────┴────────────┴─────────────┴───────────────────────┴─────────────────────────┘
JSONの例
data/users.json(配列形式のレコード):
[
{"id": 1, "name": "田中太郎", "city": "横浜"},
{"id": 2, "name": "鈴木花子", "city": "東京"}
]
これを実際にクエリすると:
SELECT * FROM read_json_auto('/db/users.json');
┌───────┬──────────┬─────────┐
│ id │ name │ city │
│ int64 │ varchar │ varchar │
├───────┼──────────┼─────────┤
│ 1 │ 田中太郎 │ 横浜 │
│ 2 │ 鈴木花子 │ 東京 │
└───────┴──────────┴─────────┘
JOINの例
users(顧客)とorders(注文)の2テーブルで確認:
CREATE OR REPLACE TABLE users (id INTEGER, name VARCHAR, city VARCHAR);
INSERT INTO users VALUES (1,'田中太郎','横浜'),(2,'鈴木花子','東京'),(3,'佐藤次郎','大阪');
CREATE OR REPLACE TABLE orders (order_id INTEGER, user_id INTEGER, item VARCHAR, price INTEGER);
INSERT INTO orders VALUES (101,1,'ノートPC',120000),(102,1,'マウス',3000),(103,2,'キーボード',8000);
INNER JOIN(注文があるユーザーだけ):
SELECT u.name, u.city, o.item, o.price
FROM users u
JOIN orders o ON u.id = o.user_id
ORDER BY u.id, o.order_id;
┌──────────┬─────────┬────────────┬────────┐
│ name │ city │ item │ price │
│ varchar │ varchar │ varchar │ int32 │
├──────────┼─────────┼────────────┼────────┤
│ 田中太郎 │ 横浜 │ ノートPC │ 120000 │
│ 田中太郎 │ 横浜 │ マウス │ 3000 │
│ 鈴木花子 │ 東京 │ キーボード │ 8000 │
└──────────┴─────────┴────────────┴────────┘
LEFT JOIN(注文が無い佐藤次郎も出てくる):
SELECT u.name, u.city, o.item, o.price
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
ORDER BY u.id;
┌──────────┬─────────┬────────────┬────────┐
│ name │ city │ item │ price │
│ varchar │ varchar │ varchar │ int32 │
├──────────┼─────────┼────────────┼────────┤
│ 田中太郎 │ 横浜 │ マウス │ 3000 │
│ 田中太郎 │ 横浜 │ ノートPC │ 120000 │
│ 鈴木花子 │ 東京 │ キーボード │ 8000 │
│ 佐藤次郎 │ 大阪 │ NULL │ NULL │
└──────────┴─────────┴────────────┴────────┘
LEFT JOIN + GROUP BY(ユーザーごとの注文数・合計金額、未購入は0件・NULL):
SELECT u.name, count(o.order_id) AS 注文数, sum(o.price) AS 合計金額
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
GROUP BY u.name
ORDER BY u.name;
┌──────────┬────────┬──────────┐
│ name │ 注文数 │ 合計金額 │
│ varchar │ int64 │ int128 │
├──────────┼────────┼──────────┤
│ 佐藤次郎 │ 0 │ NULL │
│ 田中太郎 │ 2 │ 123000 │
│ 鈴木花子 │ 1 │ 8000 │
└──────────┴────────┴──────────┘
MySQLとの比較
DuckDBは標準SQL寄りで、MySQLと共通する構文も多いが差異もある。
| やりたいこと | MySQL | DuckDB |
|---|---|---|
| CLIで接続 | mysql -u user -p db |
duckdb mydb.duckdb(本環境ではdocker compose exec duckdb duckdb /db/mydb.duckdb) |
| テーブル一覧 | SHOW TABLES; |
SHOW TABLES;(同じ構文が使える) |
| テーブル構造確認 |
DESCRIBE users; / SHOW COLUMNS FROM users;
|
DESCRIBE users;(同じ構文が使える) |
| DB一覧 | SHOW DATABASES; |
SHOW DATABASES;(ATTACHしたDB一覧が出る) |
| 文字列連結 | CONCAT(a, b) |
a || b(標準SQLの||を使う。CONCAT()関数もある) |
| 識別子のクォート | バッククォート |
"name"(ダブルクォート。バッククォートは使えない=検証済み) |
| LIMIT / OFFSET | LIMIT 10 OFFSET 5 |
LIMIT 10 OFFSET 5(同じ構文) |
| 自動採番 | id INT AUTO_INCREMENT |
CREATE SEQUENCE+DEFAULT nextval('...')(GENERATED ALWAYS AS IDENTITYは未実装=検証済み) |
| 現在時刻 | NOW() |
now()(同じ関数名で使える) |
| CSVインポート | LOAD DATA INFILE '...' INTO TABLE t; |
CREATE TABLE t AS SELECT * FROM read_csv_auto('...');(ファイルを直接クエリできるのでインポート自体が不要な場合も多い) |
| 外部DBに接続 | 標準で対応 |
mysql_scanner拡張が必要(INSTALL mysql; LOAD mysql; ATTACH '...' AS mysqldb (TYPE mysql);) |
| ストレージ形式 | 行指向(InnoDB等) | 列指向(DuckDB独自形式 + Parquetとの親和性が高い) |
| 用途の想定 | サーバー常駐のOLTP(トランザクション処理) | 単一プロセス埋め込みのOLAP(分析・集計処理) |
SHOW TABLES / DESCRIBE / LIMIT OFFSET / NOW()はMySQLとほぼ同じ書き方で動くため、単純な参照・集計クエリは移植しやすい。一方でバッククォートでの識別子クォートやMySQL固有関数(GROUP_CONCAT等)はそのままでは動かないため置き換えが必要。
動作確認
./run.sh up
./run.sh sql sample.sql testcheck.duckdb
sample.sqlはusersテーブルを作成し、都市ごとの人数・平均年齢を集計するサンプル。以下のような結果が出れば環境構築は成功。
┌─────────┬───────┬──────────┐
│ city │ 人数 │ 平均年齢 │
│ varchar │ int64 │ double │
├─────────┼───────┼──────────┤
│ 横浜 │ 2 │ 29.0 │
│ 東京 │ 1 │ 25.0 │
│ 大阪 │ 1 │ 35.0 │
└─────────┴───────┴──────────┘
data/mydb.duckdbは既にサンプルとは異なる列構成のusersテーブルを持っている場合がある(列数不一致でBinder Errorになる)。動作確認だけなら別名のDBファイル(例: testcheck.duckdb)を指定するのが安全。
決定事項
- READMEに載せる内容は原則すべて実機検証してから記載する方針とした(例:
GENERATED ALWAYS AS IDENTITYはDuckDBで未実装エラーになることを確認し、CREATE SEQUENCE方式に修正) -
data/users.jsonをサンプルとして新規作成し、既存のusers.csv/users.parquetと同様に.gitignore対象にして非管理化 - 変更はすべて
hit1023/duckdb-dockerリポジトリのmainブランチにコミット・push済み
次のTODO
- 常用する拡張(httpfs等)があればDockerfileに
INSTALL行を焼き込んで永続化を検討 - Harlequin接続やWeb UI経由での利用感を実際に試して、ワークフローに過不足がないか確認