- Published on
DuckDB Internals Deep Dive — Embedded OLAP、Vectorized Execution、Morsel-Driven Parallelism、Storage Format 完全攻略 (2025)
- Authors

- Name
- Youngju Kim
- @fjvbn20031
TL;DR
- DuckDB は CWI (Centrum Wiskunde & Informatica、オランダ) の Mark Raasveldt と Hannes Mühleisen が 2018 年に開始。「解析用 SQLite」を目標とする。
- 設計哲学: 単一ファイルフォーマット、単一プロセス、外部依存なし。
pip install duckdbだけでフル OLAP エンジン。 - ストレージ: columnar。各ブロックに compression (RLE、bitpacking、dictionary、文字列用の FSST)。
- 実行モデル: Vector ベース — 1024 行を 1 chunk として処理。SIMD フレンドリー。
- 並列処理: Morsel-Driven Parallelism — HyPer 論文のアイデア。各 morsel(~100K 行)を独立に処理。
- クエリ最適化: Cost-based join ordering、filter pushdown、projection pruning、一般的な rule-based 最適化。
- Python 統合: Pandas DataFrame / Polars / Arrow を zero-copy でアクセス。
duckdb.sql("SELECT * FROM my_df")がそのまま動く。 - Parquet/Arrow/CSV/JSON ネイティブ — コピーなしで scan。
- WASM: ブラウザでも動く。Observable notebook、Malloy、Evidence が活用。
- MotherDuck: DuckDB ベースのクラウドサービス。「hybrid execution」でローカルとクラウドを混ぜる。
1. DuckDB が解決した問題
1.1 データサイエンスの両極
2020 年前後、Python データサイエンティストの選択肢:
Pandas(ローカル、単一マシン):
- 小さなデータ(< 10 GB)に便利。
- 大きなデータでメモリ不足や性能問題。
- ほぼシングルスレッド。
Spark / Dask(分散):
- 大きなデータ(TB+)に必要。
- 小さなデータには オーバーヘッド過多。
- JVM クラスタの運用が必要。
- 起動だけで数秒。
その中間(10 GB ~ 1 TB)が 空白地帯だった。
1.2 Mark Raasveldt と Hannes Mühleisen
2018 年 CWI で 2 人は **「OLAP 版 SQLite」**を構想。以下を求めた:
- embedded(プロセスに内蔵)。
- 解析クエリで高速。
- 単一ファイルフォーマット。
- 多様なファイルフォーマット(Parquet、CSV)対応。
- Python が主な利用先だが、C/C++ API も。
2019 年に初公開。2020–2022 年に急成長。2024 年 1.0 リリース — 本番レディ。
1.3 時間が証明した選択
Pandas、Spark がある中で「さらに別の DB」が必要か? 2025 年の答え: 必要だ。
- Pandas 2.0+: DuckDB または Arrow をバックエンドに。
- Polars: 似た哲学、Rust 実装。
- Malloy、Evidence: DuckDB ベースの BI。
- Fivetran、dlt: ETL に DuckDB を内蔵。
- Mode、Hex: インタラクティブ解析に DuckDB。
- MotherDuck: DuckDB をサーバレスクラウドに。
DuckDB は「データサイエンスの新しい標準」になりつつある。
2. 設計哲学
2.1 Embedded
DuckDB は ライブラリ — Python の import duckdb、C++ のリンク、Rust の crate。
import duckdb
con = duckdb.connect('my.duckdb') # または ':memory:'
con.execute("CREATE TABLE users (id INT, name VARCHAR)")
con.execute("INSERT INTO users VALUES (1, 'Alice')")
result = con.execute("SELECT * FROM users").fetchall()
サーバプロセスなし。SQLite と同じモデル。
2.2 単一ファイル
my_data.duckdb ← 1 つのファイルにすべてのテーブル、インデックス
バックアップ、配布、共有が簡単。数 GB から数百 GB まで。
2.3 外部依存なし
単一の C++ コードでビルド可能。付随ライブラリなし。小さなバイナリ(~30 MB)。
2.4 "No Locks, No Compromise"
並行性をサポートするがシンプルに:
- Single-writer: 同時に 1 つの writer。
- Multiple readers: 読み取りは writer と並行。
- MVCC ベース。
複雑な分散 consensus なしにシンプルな実装ながら、解析ワークロードには十分。
2.5 互換性
- ANSI SQL 準拠(PostgreSQL 互換)。
- Parquet、Arrow、CSV、JSON を直接読み取り。
- Python、R、Java、Node.js、Go、Rust、WASM のバインディング。
3. ストレージ — Columnar
3.1 基本構造
各テーブルは複数の row group から成る。各 row group は 複数の column chunk。
Table "events":
Row Group 1 (122,880 rows):
Column "timestamp": [bytes...]
Column "user_id": [bytes...]
Column "event_type": [bytes...]
Column "value": [bytes...]
Row Group 2:
...
各 column chunk は 独立に圧縮される。
3.2 Compression
DuckDB の compression コーデック:
Uncompressed: 原本そのまま。
RLE(Run-Length Encoding): 繰り返し値。AAABBC → 3A 2B 1C。
Bit-packing: 整数値域に合う最小ビット。0–7 なら 3 ビットずつ。
Dictionary: 一意値辞書 + インデックス。文字列カラムに効果的。
FSST(Fast Static Symbol Table): 文字列圧縮。DuckDB チームの論文。
Chimp / Alp: 実数(float)圧縮。時系列データ向け。
各 chunk ごとに 最も効率的な compression を自動選択。
3.3 Row Group サイズ
デフォルトの row group は 122,880 行。この数字は 2^17 - 2^13 — 特定の最適化を可能にするために選ばれている。
- 小さすぎる: メタデータのオーバーヘッド。
- 大きすぎる: filter pushdown の効果が薄れる。
- 120K 程度がバランス。
3.4 Block サイズ
内部的に 256 KB ブロック。ディスク I/O の単位。
3.5 Checkpoint
書き込み後、定期的に checkpoint: メモリの WAL を実ファイルに反映。SQLite と同様。
3.6 ファイルフォーマットの安定性
2024 年の 1.0 リリースから Storage Format 安定化。以前はフォーマット変更が多かったが、今は forward/backward 互換。
4. Vector ベース実行
4.1 Volcano vs Vectorized
従来 DB(PostgreSQL):
for each row:
apply_filter(row)
transform(row)
aggregate(row)
Volcano model — 1 行ずつ処理。関数呼び出しオーバーヘッドが累積。分岐予測ミス。
Columnar + Vectorized(DuckDB、ClickHouse):
for each batch of 1024 rows:
filter_batch()
transform_batch()
aggregate_batch()
Vector model — 1024 行を一度に。SIMD フレンドリー、キャッシュ効率的。
4.2 DuckDB の Vector サイズ
1024 行 per chunk がデフォルト。理由:
- L1/L2 キャッシュに収まる: 1 カラム 1024 × 8 バイト = 8 KB。L1 = 32 KB、L2 = 256 KB。複数カラムを同時処理してもキャッシュに留まる。
- SIMD 単位: AVX-512 は 8 個の
i64を一度に。1024 / 8 = 128 イテレーション。効率的。 - 分岐予測: 繰り返し文の内部は予測しやすい。
4.3 Vector 構造
struct Vector {
Type type; // INT, VARCHAR, DOUBLE, etc.
VectorType vtype; // FLAT, CONSTANT, DICTIONARY, SEQUENCE
ValidityMask nulls; // 各行の null 有無
void* data; // 実際の値
idx_t count; // 現在の行数
};
Vector type:
- FLAT: 連続配列。
- CONSTANT: すべての行が同じ値。値 1 つだけ保存(圧縮)。
- DICTIONARY: 一意値 + インデックス。
- SEQUENCE: 等差数列(例: 0, 1, 2, ..., 1023)。
Vector type が コンパイル時に分かる と特殊経路が実行される → 性能 ↑。
4.4 Chunks
複数の vector が集まると DataChunk:
struct DataChunk {
vector<Vector> data;
idx_t count; // 現在の行数(最大 1024)
};
「1024 行の横に広いテーブル」。クエリ実行の単位。
4.5 Operator Pipeline
SELECT user_id, SUM(value)
FROM events
WHERE event_type = 'click'
GROUP BY user_id
Operator tree:
HashAggregate (GROUP BY user_id, SUM(value))
↓
Filter (event_type = 'click')
↓
TableScan (events)
各 operator が DataChunk を input、DataChunk を output:
class Operator {
virtual unique_ptr<DataChunk> GetChunk() = 0;
};
Pipeline ベース実行。Pull model(operator が上位 operator に「次の chunk をくれ」と要求)。
5. Morsel-Driven Parallelism
5.1 従来の並列モデルの問題
従来のアプローチ: exchange operator でデータを worker 間で送る。
Filter → Exchange → Aggregate
(重い)
問題:
- Shuffle コスト(データコピー)。
- Load imbalance: 一部 worker が遅い。
- 実装が複雑。
5.2 HyPer Morsel のアイデア
2014 年の Viktor Leis らの "Morsel-Driven Parallelism" 論文(HyPer DB)。
核心:
- 入力を morsel に分割 — 各 ~100K 行。
- Worker pool を用意(スレッド数)。
- 各 worker が morsel queue から取って 処理する。
- Work stealing で load balance 自動。
Operator は worker が複数いることを知らない。各自が自分の morsel を処理して output morsel を push。
5.3 DuckDB の実装
DuckDB は pipeline ベース parallelism:
Pipeline 1: TableScan → Filter → PartialHashAggregate
Pipeline 2: FinalHashAggregate → Output
各 pipeline が 並列実行。1 pipeline が完了すると次の pipeline 開始。
Pipeline 内部で morsel 単位で並列:
Input morsels: [M1] [M2] [M3] [M4] [M5] ...
Worker 1: pull M1 → process → output
Worker 2: pull M2 → process → output
Worker 3: pull M3 → process → output
Worker 4: pull M4 → process → output
...
Thread-safe queue で morsel を配る。ロックはほとんどない。
5.4 Parallel Hash Aggregate
Aggregation の並列化:
Phase 1: 各 worker がローカル morsel から partial hash table を構築
Phase 2: Partial hash table をマージ
Phase 3: Final hash table から出力
Phase 1 は完全に独立 → 線形にスケール。 Phase 2 は partition-based merge — 各 partition を独立 worker に割り当て。
5.5 結果
DuckDB は コア数にほぼ線形にスケール。4 cores → 4 倍、16 cores → 16 倍(現実のワークロードに近い場合)。
6. Query Optimizer
6.1 Rule-Based Optimizer
基本的な最適化:
- Filter pushdown: Filter を scan 側に押して早く適用。
- Projection pruning: 不要なカラムを読まない。
- Constant folding:
1 + 2のような定数計算。 - CSE(Common Subexpression Elimination)。
- Predicate simplification:
x AND FALSE→FALSE。 - Deliminator: correlated subquery 除去。
6.2 Cost-Based Join Ordering
Join 順序は性能に大きな影響:
A (1M) JOIN B (100) JOIN C (10)
- A JOIN B 先: 1M × 100 = 100M 中間結果。
- B JOIN C 先: 100 × 10 = 1K 中間、その後 1K JOIN A。
2 番目が圧倒的に速い。
DuckDB は dynamic programming ベースの join ordering。テーブルのカーディナリティ + 条件の selectivity でコスト推定。
6.3 Statistics
基本統計:
- 行数。
- 一意値数(HyperLogLog で近似)。
- 各カラムの min/max。
- Null 比率。
ANALYZE なしでも scan 中に自動収集。
6.4 Zonemap / Pruning
Row group ごとに min/max を保存。クエリ例:
SELECT * FROM events WHERE timestamp > '2024-01-01'
各 row group の max timestamp をチェック → 条件を満たさなければ skip。実データを読まない。
Parquet のようなフォーマットの predicate pushdown と類似。
6.5 Late Materialization
Column store の利点を活用:
SELECT name, email FROM users WHERE age > 30
- Early materialization: すべてのカラムを読んでから filter → 無駄。
- Late materialization:
ageだけ先に読んで filter → マッチする行 index だけ保存 → その index だけでname、emailを取得 → 効率的。
DuckDB はデフォルトで late materialization。
7. Python 統合
7.1 Zero-Copy DataFrame
DuckDB のキラー機能:
import duckdb
import pandas as pd
df = pd.DataFrame({
'name': ['Alice', 'Bob'],
'age': [30, 25]
})
# DataFrame を SQL から直接クエリ
result = duckdb.sql("SELECT * FROM df WHERE age > 28").df()
# ^^ Python 変数をテーブルとして
print(result)
どうやって? DuckDB が Python interpreter の変数を inspect して DataFrame に zero-copy でアクセスする。Pandas 内部の Arrow または NumPy バッファを直接読む。
7.2 Polars と Arrow も
import polars as pl
df = pl.DataFrame({'a': [1,2,3]})
duckdb.sql("SELECT * FROM df").pl() # Polars の結果
import pyarrow as pa
tbl = pa.table({'a': [1,2,3]})
duckdb.sql("SELECT * FROM tbl").arrow()
すべての frame ライブラリと 相互運用。
7.3 Relational API
SQL の代わりに Python API で:
rel = duckdb.sql("SELECT * FROM df")
.filter("age > 28")
.project("name, age * 2 as double_age")
.aggregate("SUM(double_age)")
Lazy evaluation — .df()、.pl()、.fetchall() を呼ぶまで実行されない。Optimizer が全体プランを見て最適化。
7.4 Jupyter Notebook
# cell 1
import duckdb
con = duckdb.connect(':memory:')
# cell 2
%load_ext sql # ipython-sql 拡張
%sql duckdb:///my.duckdb
# cell 3
%%sql
SELECT * FROM my_table LIMIT 10
SQL を直接 Jupyter に書ける。解析ワークフローが滑らか。
7.5 Pandas 2.0+ 統合
Pandas 2.0 は Arrow バックエンド をサポート:
df = pd.read_parquet('data.parquet', dtype_backend='pyarrow')
この df は Arrow メモリ構造。DuckDB が zero-copy でアクセス。
今後「Pandas → DuckDB → 結果 → Pandas」パイプラインが標準になる可能性。
8. Parquet / Arrow 統合
8.1 Parquet を直接読む
SELECT * FROM 'data.parquet' WHERE year = 2024
ファイルをテーブルのように扱う。DuckDB は:
- Parquet footer のパース。
- Row group metadata の確認。
- Predicate pushdown — 該当 row group だけ読む。
- Projection pushdown — 必要なカラムだけ。
- 並列 scan。
8.2 複数ファイルを同時に
SELECT * FROM 'logs/*.parquet'
SELECT * FROM read_parquet(['f1.parquet', 'f2.parquet'])
Hive partitioning 対応:
data/year=2024/month=01/day=01/part.parquet
WHERE year = 2024 → 自動的に該当ディレクトリだけスキャン。
8.3 S3 / HTTP
SELECT * FROM 's3://bucket/data.parquet'
SELECT * FROM 'https://example.com/data.csv'
httpfs extension でリモートファイルを直接扱える。Range request で必要なバイトだけダウンロード。
8.4 CSV / JSON
SELECT * FROM 'data.csv' -- 自動スキーマ検出
SELECT * FROM read_json('data.json', auto_detect=true)
Smart CSV parser — Unicode BOM、多様な区切り文字、quoted field をすべて処理。
JSON も nested をサポート:
SELECT item->>'name' AS name, (item->>'price')::FLOAT AS price
FROM 'items.json'
8.5 Arrow 結果
result = con.execute("SELECT * FROM t").fetch_arrow_table()
Zero-copy で Arrow テーブルを返す。DuckDB 内部 vector はすでに Arrow 互換。
9. WASM とブラウザ
9.1 DuckDB-WASM
DuckDB は WebAssembly にコンパイル可能。ブラウザで直接解析 DB:
<script type="module">
import * as duckdb from '@duckdb/duckdb-wasm';
const JSDELIVR_BUNDLES = duckdb.getJsDelivrBundles();
const bundle = await duckdb.selectBundle(JSDELIVR_BUNDLES);
const worker = new Worker(bundle.mainWorker);
const logger = new duckdb.ConsoleLogger();
const db = new duckdb.AsyncDuckDB(logger, worker);
await db.instantiate(bundle.mainModule, bundle.pthreadWorker);
const conn = await db.connect();
const result = await conn.query(`
SELECT * FROM 'https://example.com/data.parquet' LIMIT 10
`);
</script>
サーバなしでクライアント側解析。Observable、Malloy、Evidence のようなツールが活用。
9.2 ユースケース
- Observable notebook: ブラウザで DuckDB を使い JSON/CSV/Parquet を解析。
- Evidence: 静的ダッシュボード。ビルド時に DuckDB で集計し、HTML を配布。
- Static BI: データを Parquet で配布し、ブラウザで SQL。
「サーバレス BI」が可能になった。
9.3 性能
Native と比べると 20–50% 遅いが、それでも実用的。100 MB Parquet を数秒で解析。
10. Python ワークフロー例
10.1 置き換えシナリオ
Before(Pandas):
import pandas as pd
df = pd.read_parquet('large.parquet')
grouped = df[df['event_type'] == 'click'].groupby('user_id')['value'].sum()
top10 = grouped.nlargest(10)
- ファイル全体をメモリにロード。
- シングルスレッド。
- 大きなデータで遅い。
After(DuckDB):
import duckdb
result = duckdb.sql("""
SELECT user_id, SUM(value) AS total
FROM 'large.parquet'
WHERE event_type = 'click'
GROUP BY user_id
ORDER BY total DESC
LIMIT 10
""").df()
- ファイルをストリーミング(全ロードなし)。
- 並列実行。
- 10 倍以上速い。
- 同じ Pandas DataFrame 結果。
10.2 ETL パイプライン
con = duckdb.connect('warehouse.duckdb')
# Extract: API/ファイルから
con.execute("""
CREATE OR REPLACE TABLE raw_events AS
SELECT * FROM read_parquet('s3://bucket/events/*.parquet')
""")
# Transform
con.execute("""
CREATE OR REPLACE TABLE clean_events AS
SELECT
user_id,
event_type,
CAST(timestamp AS TIMESTAMP) AS ts,
value
FROM raw_events
WHERE value > 0
""")
# Load
con.execute("COPY clean_events TO 'output.parquet' (FORMAT PARQUET)")
ローカルで 数 GB ~ 数十 GB の ETL が可能。Spark は不要。
10.3 Jupyter 解析
%load_ext sql
%sql duckdb:///analysis.duckdb
%%sql
SELECT
date_trunc('month', purchase_date) AS month,
category,
SUM(amount) AS revenue
FROM sales
GROUP BY 1, 2
ORDER BY 1, 2
Notebook が SQL-first。Pandas より SQL の方が表現力があるケース — 特に window function、CTE、join。
10.4 dbt との統合
dbt-duckdb adapter:
# profiles.yml
my_project:
target: dev
outputs:
dev:
type: duckdb
path: /tmp/my.duckdb
dbt のすべての機能(models、tests、snapshots)を ローカルの DuckDB で。開発/テストが速い。本番は Snowflake などそのまま。
11. Polars との関係
11.1 2 つのプロジェクト
Polars: Ritchie Vink の Rust ベース DataFrame ライブラリ。 DuckDB: C++ ベース SQL エンジン。
同じ問題(高速なローカル OLAP)を異なるインターフェースで。
11.2 Polars のエンジン
実は Polars の初期 query engine は DuckDB チームと共同設計された。Expressions、lazy evaluation、最適化パターンが似ている。
2024 年から Polars は自前エンジン(新しい streaming engine)への移行中。しかし多くのアイデアは DuckDB 由来。
11.3 比較
| 項目 | DuckDB | Polars |
|---|---|---|
| 言語 | C++ | Rust |
| インターフェース | SQL | DataFrame API |
| ファイルフォーマット | 独自 .duckdb | Parquet または独自 |
| Python 統合 | Zero-copy | Native |
| 解析機能 | 豊富(SQL 標準) | Polars API |
| エコシステム | dbt、BI ツール | ML パイプライン |
どちらも優秀。SQL が楽なら DuckDB、DataFrame API が楽なら Polars。
11.4 併用
import polars as pl
import duckdb
# Polars DataFrame
df = pl.read_parquet('data.parquet')
# DuckDB で SQL クエリ
result = duckdb.sql("SELECT name, SUM(value) FROM df GROUP BY name").pl()
# ^^
# Polars 結果として返す
相互運用性が優れる。同じ Arrow メモリベース。
12. 拡張(Extensions)
12.1 Extension System
DuckDB は 拡張可能なアーキテクチャ:
INSTALL httpfs;
LOAD httpfs;
-- これで S3 アクセス可能
SELECT * FROM 's3://bucket/file.parquet';
Extension は動的ライブラリ。ランタイムでロード。
12.2 主要な Extensions
- httpfs: HTTP/S3 アクセス。
- postgres_scanner: Postgres テーブルを直接クエリ。
- mysql_scanner: MySQL も。
- sqlite_scanner: SQLite テーブルアクセス。
- iceberg: Apache Iceberg テーブル。
- delta: Delta Lake テーブル。
- fts: Full-text search。
- json: 高度な JSON 関数。
- spatial: GIS 関数。
- vss(Vector Similarity Search): 埋め込み検索(HNSW)。
- icu: 国際化。
数十個。エコシステムが豊富。
12.3 サードパーティ
コミュニティが作った extension もロード可能:
INSTALL 'my_extension' FROM 'https://example.com/';
LOAD 'my_extension';
Signing をサポート。安全なロード。
12.4 Iceberg / Delta
現代のデータレイクフォーマットをサポート:
SELECT * FROM iceberg_scan('s3://bucket/iceberg-table');
他のエンジン(Spark、Trino)が書いたテーブルを DuckDB で読む。エンジン間の相互運用性。
13. MotherDuck
13.1 会社
2022 年、DuckDB Labs の一部創業者が MotherDuck 社を設立。DuckDB ベースのマネージドサービス。
13.2 Hybrid Execution
核心のアイデア: ローカルとクラウドのハイブリッド。
ローカル(ノート PC):
- ユーザー対話
- 小さなデータキャッシュ
- 最近のクエリ
クラウド(MotherDuck):
- 大きなデータセット保存
- 共有データ
- 重い計算
クエリが「ローカルデータ + クラウドデータ」を join 可能。Optimizer が どこで実行するか を決める。
13.3 Use Case
小さなチーム:
- MotherDuck に共用データセット。
- 各自のノート PC で DuckDB で解析。
- 同じクエリがどちらでも動く。
13.4 Free Tier
MotherDuck は 無料ティア を提供。小さなチームに非常に魅力的な価格。
Snowflake、BigQuery の低コスト代替として地位を築きつつある。
14. 性能ベンチマーク
14.1 TPC-H
標準 OLAP ベンチマーク。DuckDB は TPC-H を積極的に活用。
Single-machine 性能比較(1TB スケール、近似):
DuckDB: 1x (baseline)
PostgreSQL: 20-50x slower
SQLite: 100x+ slower
ClickHouse: 0.5-0.8x (やや速い)
DuckDB は 単一マシン性能 に最適化。分散は MotherDuck またはパーティション組み合わせ。
14.2 Pandas 比較
単純な GroupBy 例(10 GB Parquet):
Pandas: 45 秒(メモリ不足リスク)
Polars: 12 秒
DuckDB: 8 秒
DuckDB が一般的に最速。ただし小さなデータ(100 MB 未満)は Pandas のオーバーヘッドが小さく同程度。
14.3 Spark 比較
入力: 10 GB CSV
Spark (local mode): 120 秒(JVM 起動 + plan + execute)
Spark (cluster): 40 秒(クラスタコストあり)
DuckDB: 8 秒
小規模データでは DuckDB が 圧倒的に速い。Spark の起動オーバーヘッドが小さなワークロードには過大。
1 TB 級では Spark クラスタが有利になり得るが、DuckDB も驚くほどの性能。
15. チューニング
15.1 メモリ
SET memory_limit='8GB';
デフォルト: システムの 80%。小さなマシンでは明示的に制限。
大きなデータでメモリ超過時は disk spill — temp file を使う。
15.2 スレッド
SET threads=8;
デフォルト: CPU コア数。制御が必要なら明示。
15.3 Temp ディレクトリ
SET temp_directory='/fast/ssd/tmp';
Disk spill 用。高速 SSD に置くと性能 ↑。
15.4 Profiling
EXPLAIN ANALYZE SELECT * FROM t WHERE x > 10;
実際の実行時間、行数、operator 別のコストを表示。
PRAGMA enable_profiling;
各クエリの詳細プロファイル。
15.5 チューニング Tips
- 必要なカラムだけ SELECT(projection pruning の活用)。
- WHERE 句をできるだけ早く(predicate pushdown)。
- 大きな join には
ORDER BY+LIMIT: top-k だけ必要ならソートの一部だけ。 - インデックスよりパーティショニング: row group skipping でほとんどのケースは十分。
COPYfor bulk load: INSERT より 100 倍速い。
16. 学習リソース
公式:
- https://duckdb.org/docs/ — 公式ドキュメント。
- https://duckdb.org/community/ — ブログ、Discord。
動画:
- Hannes Mühleisen、Mark Raasveldt の発表(CIDR、SIGMOD)。
- "DuckDB under the hood" シリーズ。
書籍:
- Mark Raasveldt と Hannes Mühleisen の論文群。
- CWI Database Architecture グループの論文群。
例:
- https://duckdb.org/docs/guides/overview。
- DuckDB Python tutorial。
- Malloy + DuckDB チュートリアル。
ビデオ:
- CMU Database Group 講義の Vectorized Execution 回。
17. まとめ — 1 枚の整理
┌─────────────────────────────────────────────────────┐
│ DuckDB Cheat Sheet │
├─────────────────────────────────────────────────────┤
│ 哲学: │
│ Embedded OLAP database │
│ "SQLite for analytics" │
│ 単一ファイル、単一プロセス │
│ No server, no lock │
│ │
│ ストレージ: │
│ columnar │
│ Row groups (122,880 rows) │
│ Compression: RLE, bitpack, dict, FSST │
│ Checkpoint + WAL │
│ │
│ 実行: │
│ Vector-based (1024 rows per chunk) │
│ SIMD friendly │
│ Late materialization │
│ Pipeline operators │
│ │
│ 並列: │
│ Morsel-Driven Parallelism │
│ ~100K rows per morsel │
│ Work stealing │
│ ほぼ線形スケール │
│ │
│ Optimizer: │
│ Rule-based (pushdown, pruning, CSE) │
│ Cost-based join ordering │
│ HyperLogLog distinct count │
│ Row group skipping (min/max) │
│ │
│ Python: │
│ pip install duckdb │
│ Zero-copy DataFrame アクセス (Pandas, Polars, Arrow)│
│ Relational API (lazy) │
│ Jupyter ipython-sql 統合 │
│ │
│ ファイル統合: │
│ Parquet (predicate pushdown) │
│ Arrow (zero-copy) │
│ CSV (smart parser) │
│ JSON (nested 対応) │
│ S3 / HTTP (httpfs extension) │
│ │
│ ブラウザ: │
│ DuckDB-WASM │
│ Observable, Malloy, Evidence │
│ │
│ Extensions: │
│ httpfs, postgres_scanner, iceberg, delta │
│ fts, spatial, vss (vector search) │
│ │
│ MotherDuck: │
│ マネージドサービス │
│ Hybrid execution (local + cloud) │
│ 無料ティア │
│ │
│ vs 競合: │
│ Pandas: 10 倍速い、大きなデータ OK │
│ Polars: 似た哲学、DataFrame API │
│ SQLite: OLAP でずっと速い │
│ ClickHouse: DuckDB は embedded │
│ Spark: DuckDB は単一マシンで速い │
│ │
│ チューニング: │
│ memory_limit, threads │
│ EXPLAIN ANALYZE │
│ Projection + predicate pushdown │
│ COPY for bulk load │
└─────────────────────────────────────────────────────┘
18. クイズ
Q1. DuckDB が「SQLite for analytics」と呼ばれる理由は?
A. 同じ配布/使用モデル、異なるワークロード最適化。SQLite が OLTP(トランザクション、小さな読み書き、多数の同時ユーザー)に embedded DB を提供したのに対し、DuckDB は OLAP(大きな scan、集計、複雑クエリ)に同じモデルを適用した。共通点: (1) ライブラリ形式 — サーバなし、pip install で完了、(2) 単一ファイルフォーマット — バックアップ/移動が簡単、(3) 外部依存なし — 小さなバイナリ、(4) ACID + MVCC。相違: SQLite は 行ベース ストレージ(B-tree)、DuckDB は columnar ストレージ + ベクトル化実行。同じ「embedded DB」哲学を異なるワークロードに適用したことが DuckDB の genius。2020 年代のデータサイエンティストが求めた「小さなサーバなしで SQL 解析」への答え。
Q2. Morsel-Driven Parallelism が伝統的な exchange ベース並列と違う点は?
A. データ移動の最小化と work stealing。従来の並列 DB(Spark、PostgreSQL parallel query)は exchange operator で worker 間にデータを shuffle — ネットワークコストが大きく、shuffle 後の load imbalance(一部 worker が遅い)も残る。Morsel-Driven: 入力を 小さな morsel(~100K rows)に分割し、worker pool が queue から取る。各 worker が自分のペースで pull → 速い worker がより多くの morsel を処理 → 自動的な load balance。Shuffle なし、各 morsel は独立。2014 年 Leis らの HyPer 論文が提案、DuckDB/Umbra/Vectorwise が採用。利点: (1) 実装がシンプル(lock-free queue)、(2) 線形スケール、(3) long-tail 除去。単一マシンで数十コアを活用するのに最適。
Q3. DuckDB の vector が 1024 行である理由は?
A. L1/L2 キャッシュ最適化 + SIMD 効率。小さすぎる(例: 16 rows)と関数呼び出し/分岐予測オーバーヘッドが支配的 → Volcano モデルの欠点の再現。大きすぎる(例: 100K rows)と 1 カラムが L1/L2 を超えて spill。1024 rows × 8 bytes = 8 KB per column — L1(32 KB)に複数カラムが同時に存在可能。AVX-512 基準で 8 × int64 を一度に処理 → 1024 / 8 = 128 iterations — 分岐予測が容易。また row group size 122,880 = 2^17 - 2^13 という特定の数字も compression + vector サイズの組み合わせで最適になるよう設計されている。「魔法の数字」ではなく ハードウェア特性 に基づく。ClickHouse は 65,536、DuckDB は 1024 — それぞれ異なる trade-off。「行ベース Volcano → column-at-a-time → vector-at-a-time」という進化の現時点での sweet spot。
Q4. DuckDB が Python DataFrame に zero-copy でアクセスする方法は?
A. Arrow メモリ表現の活用。Pandas 2.0+、Polars、PyArrow はすべて Arrow columnar memory layout を内部または外部表現として使う。DuckDB 内部 vector も Arrow 互換。duckdb.sql("SELECT * FROM df") 呼び出し時に DuckDB は: (1) Python interpreter の locals から df 変数を検索(replacement scan)、(2) DataFrame 型を検出、(3) 内部 Arrow バッファポインタを取得、(4) DuckDB vector がその 同じメモリ を指すようにする。データコピーなし → 巨大な DataFrame でも遅延なし。Pandas 1.x(NumPy ベース)は一部コピーが必要だが、2.0+ の Arrow バックエンドは真の zero-copy。逆方向も同様 — .df()、.pl()、.arrow() の結果も zero-copy。結果: Python と SQL が透過的に統合 — これが Pandas 置き換え加速の核心的な技術的根拠。
Q5. DuckDB の row group size (122,880) がなぜ特別なのか?
A. Compression + vector サイズ + row group skipping のバランス点。小さくする(例: 10K)と row group メタデータのオーバーヘッドが大きくなり、圧縮効率が落ちる。大きくする(例: 1M)と predicate pushdown で skip できる単位が粗すぎて selective クエリで 必要以上のデータを読む ことになる。122,880 = 120 × 1024 — 120 個の vector が 1 row group。このサイズで (1) compression dictionaries が十分大きく、(2) min/max statistics が意味のある skipping を可能にし、(3) メタデータオーバーヘッドが総サイズの 1% 未満。Parquet のデフォルト row group(1M rows)は bulk analytics 向けに最適、DuckDB の 122,880 は point-ish query にも対応。同じ理由で DuckDB は Parquet を読む時も 120K 単位でカットして処理する。設計判断の細部が実際のクエリ性能に与える影響の好例。
Q6. DuckDB-WASM が可能にした新しいユースケースは?
A. サーバレス BI と静的ダッシュボード。従来 BI はサーバ + DB + 認証インフラが必要 — 小さなチーム/個人には過大。DuckDB-WASM: (1) Parquet/CSV を static file としてホスト(GitHub Pages、S3、CloudFront)、(2) ブラウザの DuckDB-WASM が range request で必要なバイトだけダウンロード、(3) SQL がブラウザで実行、(4) 結果を JS で可視化。Evidence.dev がこのパターン: Markdown + SQL をビルド時に HTML/JSON として静的生成、訪問者がブラウザでクエリ可能。Observable notebook: インタラクティブ解析、すべてクライアントサイド。Mosaic(UW IDL): 数百万行のインタラクティブ可視化。「小さなサービス + 静的ファイル + DuckDB-WASM」がシンプルな解析アプリの新しいアーキテクチャ。インフラコストほぼ 0、スケーラビリティ無限(CDN)、プライバシー優秀(データがサーバに行く必要なし)。Snowflake の対極。
Q7. DuckDB と Polars は競合か補完か?
A. 両方 — 同じ問題を異なる角度から解く補完的ツール。共通点: (1) ローカル/embedded OLAP、(2) columnar + ベクトル化、(3) Arrow メモリ互換、(4) Pandas 置き換えの目標。相違: DuckDB は SQL エンジン(C++)、Polars は DataFrame API(Rust)。ユーザー好みに従う — SQL が楽なら DuckDB、method chaining が楽なら Polars。興味深い歴史: Polars の初期 query engine のアイデアは DuckDB チームと共有された(expressions、lazy eval、最適化)。2024 年 Polars が自前 streaming engine に移行中だが、ルーツは同じ。実務パターン: 併用。Polars で DataFrame 操作 → duckdb.sql() で SQL クエリ → Polars DataFrame で返してもらう。同じ Arrow メモリで zero-copy。「どちらか」ではなく「両方」が正解。Pandas(順次、シングルスレッド)を置き換える共同戦線。
この記事が役に立ったら、次の投稿もチェックしてみてください:
- "ClickHouse Internals Deep Dive" — サーバベースの columnar OLAP。
- "SQLite Internals Deep Dive" — embedded OLTP の教科書。
- "Apache Arrow & Columnar Memory" — 共通メモリフォーマット。
- "Columnar Storage (Parquet/ORC/Arrow/Dremel)" — ファイルフォーマット深掘り。