Skip to content

✍️ 필사 모드: DuckDB Internals Deep Dive — Embedded OLAP、Vectorized Execution、Morsel-Driven Parallelism、Storage Format 完全攻略 (2025)

日本語
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.

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.duckdb1 つのファイルにすべてのテーブル、インデックス

バックアップ、配布、共有が簡単。数 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): 繰り返し値。AAABBC3A 2B 1CBit-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 間で送る。

  FilterExchangeAggregate
            (重い)

問題:

  • Shuffle コスト(データコピー)。
  • Load imbalance: 一部 worker が遅い。
  • 実装が複雑。

5.2 HyPer Morsel のアイデア

2014 年の Viktor Leis らの "Morsel-Driven Parallelism" 論文(HyPer DB)。

核心:

  1. 入力を morsel に分割 — 各 ~100K 行。
  2. Worker pool を用意(スレッド数)。
  3. 各 worker が morsel queue から取って 処理する。
  4. Work stealing で load balance 自動。

Operator は worker が複数いることを知らない。各自が自分の morsel を処理して output morsel を push。

5.3 DuckDB の実装

DuckDB は pipeline ベース parallelism:

Pipeline 1: TableScanFilterPartialHashAggregate
Pipeline 2: FinalHashAggregateOutput

各 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 FALSEFALSE
  • 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 だけで nameemail を取得 → 効率的。

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 は:

  1. Parquet footer のパース。
  2. Row group metadata の確認。
  3. Predicate pushdown — 該当 row group だけ読む。
  4. Projection pushdown — 必要なカラムだけ。
  5. 並列 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 比較

項目DuckDBPolars
言語C++Rust
インターフェースSQLDataFrame API
ファイルフォーマット独自 .duckdbParquet または独自
Python 統合Zero-copyNative
解析機能豊富(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:  12DuckDB:  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 でほとんどのケースは十分。
  • COPY for bulk load: INSERT より 100 倍速い。

16. 学習リソース

公式:

動画:

  • Hannes Mühleisen、Mark Raasveldt の発表(CIDR、SIGMOD)。
  • "DuckDB under the hood" シリーズ。

書籍:

  • Mark Raasveldt と Hannes Mühleisen の論文群。
  • CWI Database Architecture グループの論文群。

:

ビデオ:

  • 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, FSSTCheckpoint + 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-WASMObservable, Malloy, Evidence│                                                       │
Extensions:│   httpfs, postgres_scanner, iceberg, delta           │
│   fts, spatial, vss (vector search)│                                                       │
MotherDuck:│   マネージドサービス                                    │
Hybrid execution (local + cloud)│   無料ティア                                           │
│                                                       │
│ vs 競合:Pandas: 10 倍速い、大きなデータ OKPolars: 似た哲学、DataFrame APISQLite: OLAP でずっと速い                            │
ClickHouse: DuckDB は embedded                     │
Spark: DuckDB は単一マシンで速い                     │
│                                                       │
│ チューニング:│   memory_limit, threads                              │
EXPLAIN ANALYZEProjection + 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)" — ファイルフォーマット深掘り。

현재 단락 (1/529)

- **DuckDB** は CWI (Centrum Wiskunde & Informatica、オランダ) の Mark Raasveldt と Hannes Mühleisen が 2018 ...

작성 글자: 0원문 글자: 20,085작성 단락: 0/529