0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

DuckDBをDocker環境で使う 〜Web UI/CLIセットアップからMySQLとの比較まで〜

0
Posted at

概要

~/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から使う

  1. docker compose up -d(または./run.sh up)で起動
  2. ブラウザで http://localhost:4213 を開く
  3. ノートブック内で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

対応フォーマット

標準でjsonparquet拡張が入っており、追加インストールなしで以下がすぐ使える。

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)バイナリ形式。カラムごとにまとめて圧縮して保持するため、catxxdでそのまま人間が読める内容にはならない。

  • 列指向: 行ではなく列単位でデータをまとめて格納するので、特定の列だけを読む集計クエリが高速・省メモリ
  • バイナリ+圧縮: 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経由での利用感を実際に試して、ワークフローに過不足がないか確認
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?