- Authors

- Name
- Youngju Kim
- @fjvbn20031
- はじめに — HNの常連になったアヒル
- DuckDBとは何か — OLAPのSQLite
- アーキテクチャ — なぜ速いのか
- インストールなしで始める
- ファイルの直接クエリ — DuckDBのキラー機能
- 実践クエリ例集
- pandasとPolars — 競争ではなく共生
- 拡張エコシステム
- データエンジニアリングにおける位置づけ — 軽量ETLとdbt
- MotherDuck — ハイブリッド実行という折衷案
- DuckLakeとレイクハウスの潮流
- 日常レシピ集 — 手に馴染ませておきたいもの
- 限界 — 何に使ってはいけないか
- 選択ガイド — Postgres、SQLite、ClickHouseとの比較
- パフォーマンスのヒント
- おわりに
- 参考資料
はじめに — HNの常連になったアヒル
Hacker Newsには定期的にトップに上がる定番テーマがあります。SQLite礼賛、Postgresで十分という記事、そしてDuckDB活用記です。「ノートPCで数億行を数秒で集計した」「データウェアハウスなしで分析パイプラインを全部終わらせた」といったタイトルが上がるたび、コメント欄は同じ結論に収束します。私たちのデータは思ったより小さく、DuckDBは思ったより速い、ということです。
2026年の文脈では、もう1つの理由が加わりました。AIコーディングエージェントの普及により「エージェントがデータを直接探索する」ワークフローが一般化しましたが、サーバーのインストールも認証設定もなしに、ファイル1つでSQL分析環境が作れるDuckDBは、エージェントフレンドリーなツールの代表例になりました。クラウドコスト削減の圧力とビッグテック疲労という時代の空気も、「自分のノートPCで完結する分析」の魅力を高めています。
本記事では、DuckDBが愛される構造的な理由から、実践的なクエリパターン、データエンジニアリングにおける位置づけ、そして限界まで、実務の視点で整理します。
DuckDBとは何か — OLAPのSQLite
DuckDBを一文で定義するなら「組み込み分析データベース」です。SQLiteがアプリケーションの中にライブラリとして埋め込まれてトランザクション処理を担うように、DuckDBはプロセスの中に埋め込まれて分析クエリを担当します。比較軸を整理するとこうなります。
- SQLite: 組み込み、行指向、OLTP(多数の小さな読み書き)に最適
- DuckDB: 組み込み、列指向、OLAP(少数の重い集計)に最適
- PostgresとMySQL: クライアントサーバー、行指向、OLTP
- ClickHouseとBigQuery: クライアントサーバー(または分散)、列指向、OLAP
つまりDuckDBは「サーバーレスなClickHouse」であり「分析用のSQLite」という、空いていた象限を正確に埋めたプロジェクトです。オランダCWI研究所のデータベース研究グループから出発し、2024年にバージョン1.0に到達、以降は安定性とエコシステムが急速に成熟しました。
アーキテクチャ — なぜ速いのか
DuckDBの速度は3つの設計から生まれます。
+--------------------------------------------------------------+
| ユーザープロセス (Python, CLI, Node, JVM, ...) |
| |
| +------------------------------------------------------+ |
| | DuckDB (ライブラリとして組み込み、サーバーなし) | |
| | | |
| | SQLパーサー / オプティマイザ | |
| | | | |
| | v | |
| | ベクトル化実行エンジン | |
| | - 一度に行1つではなくベクトル(約2048個の値)を処理 | |
| | - CPUキャッシュフレンドリー、SIMD活用 | |
| | - 全コアへの自動並列化 | |
| | | | |
| | v | |
| | 列指向ストレージ | |
| | - 単一ファイルDB (ACID, MVCC) | |
| | - 列ごとの圧縮 (辞書、RLE、ビットパッキング) | |
| | - Parquet/CSV/JSONの直接スキャン (インポート不要) | |
| +------------------------------------------------------+ |
| |
| メモリを超えるデータはディスクへスピル (out-of-core実行) |
+--------------------------------------------------------------+
第一に、列指向ストレージです。分析クエリは通常、数十個の列のうち2、3個しか読みません。列単位で保存すれば必要な列だけディスクから読めばよく、同じ型の値が並ぶため圧縮効率も高くなります。
第二に、ベクトル化実行です。行を1つずつ処理する伝統的なVolcanoモデルの代わりに、約2048個の値のベクトル単位で演算子を通過させます。関数呼び出しのオーバーヘッドが数千分の一に減り、CPUキャッシュとSIMD命令を効率的に活用します。
第三に、単一ファイルとout-of-core処理です。データベース全体がファイル1つで、メモリより大きいデータは自動的にディスクへ中間結果を流しながら処理します。「メモリに入らなければ終わり」のpandasとの決定的な違いです。
インストールなしで始める
DuckDBの参入障壁は実質ゼロです。
# CLIのインストール (macOS)
brew install duckdb
# インメモリモードで即実行
duckdb
# ファイルDBとして開く (なければ作成)
duckdb analytics.duckdb
# Python: pip install duckdb の1行で完了
import duckdb
# 接続なしで直接クエリ (インメモリ)
duckdb.sql("SELECT 42 AS answer").show()
# ファイルDBの使用
con = duckdb.connect("analytics.duckdb")
con.sql("CREATE TABLE t AS SELECT * FROM range(1000000)")
サーバーデーモンも、ポート設定も、ユーザーアカウントもありません。この摩擦のなさが「とりあえずDuckDBで開いてみよう」という習慣を作り、その習慣が人気の土台になりました。
ファイルの直接クエリ — DuckDBのキラー機能
DuckDBはデータを「インポート」せずに、ファイルをテーブルのように直接クエリします。
-- CSVを直接クエリ (スキーマ自動推論)
SELECT count(*) FROM 'events-2026-06.csv';
-- 複数のParquetファイルをグロブパターンで一括処理
SELECT user_id, sum(amount) AS total
FROM 'data/sales/*.parquet'
GROUP BY user_id
ORDER BY total DESC
LIMIT 10;
-- JSON Linesも直接
SELECT payload.event_type, count(*)
FROM read_json_auto('logs/*.jsonl')
GROUP BY 1;
-- ファイル形式間の変換もSQL 1行
COPY (SELECT * FROM 'raw.csv') TO 'clean.parquet' (FORMAT parquet);
httpfs拡張をロードすれば、リモートファイルも同じ構文で扱えます。
INSTALL httpfs;
LOAD httpfs;
-- HTTP URLを直接クエリ
SELECT * FROM 'https://example.com/open-data/stats.parquet' LIMIT 5;
-- S3バケットのParquetをノートPCから直接集計
SET s3_region = 'ap-northeast-2';
SELECT date_trunc('day', ts) AS d, count(*)
FROM 's3://my-bucket/events/year=2026/month=06/*.parquet'
GROUP BY d ORDER BY d;
Parquetの列統計と行グループメタデータを活用して必要な部分だけを読むため、数十GBのファイルでもフィルタが効けば数秒で答えが返ります。「データレイクをウェアハウスにロードせず、その場でクエリする」という潮流の中心にこの機能があります。
実践クエリ例集
分析SQLで頻出するパターン3つをDuckDB構文で整理します。
ウィンドウ関数 — 移動平均とランキング
-- 日次売上の7日移動平均と前日比
WITH daily AS (
SELECT date_trunc('day', ts) AS d, sum(amount) AS revenue
FROM 'sales/*.parquet'
GROUP BY d
)
SELECT
d,
revenue,
avg(revenue) OVER (
ORDER BY d ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
) AS ma7,
revenue - lag(revenue) OVER (ORDER BY d) AS diff_prev_day,
rank() OVER (ORDER BY revenue DESC) AS best_day_rank
FROM daily
ORDER BY d;
PIVOT — 行を列に
DuckDBはPIVOTを専用構文でサポートしています。列の値を1つずつ列挙する必要はありません。
-- カテゴリ別の月次売上をワイドフォーマットに
PIVOT (
SELECT category, strftime(ts, '%Y-%m') AS month, amount
FROM 'sales/*.parquet'
)
ON month
USING sum(amount)
GROUP BY category;
-- 逆方向のUNPIVOTもサポート
UNPIVOT monthly_wide
ON COLUMNS(* EXCLUDE (category))
INTO NAME month VALUE amount;
ASOF JOIN — 時系列の最も近い過去とのマッチング
相場データと約定データのように、タイムスタンプが正確に一致しない時系列を結合するときに真価を発揮します。
-- 各取引に対して「取引時点より前の最新為替レート」をマッチング
SELECT t.trade_id, t.ts, t.amount_usd, r.rate AS usdkrw_at_trade
FROM trades t
ASOF JOIN fx_rates r
ON t.ts >= r.ts;
通常のJOINでこれを実装するには相関サブクエリやウィンドウ関数の曲芸が必要ですが、ASOF JOINは意図をそのまま構文で表現しながら、実行計画も効率的です。
pandasとPolars — 競争ではなく共生
DuckDBはデータフレームライブラリを置き換えるのではなく補完します。鍵はApache Arrowベースのzero-copy連携です。
import duckdb
import pandas as pd
df = pd.read_parquet("events.parquet")
# pandas DataFrameをコピーなしでSQLクエリ (変数名をそのまま認識)
result = duckdb.sql("""
SELECT user_id, count(*) AS cnt
FROM df
WHERE event_type = 'purchase'
GROUP BY user_id
HAVING cnt >= 3
""").df() # 結果を再びpandasへ
# Polarsとも同様に往復可能
import polars as pl
pdf = duckdb.sql("SELECT * FROM 'big.parquet'").pl() # Arrow経由、コピー最小化
実務でのパターンはだいたいこうです。重いスキャン、結合、集計はDuckDBがディスク上で直接実行し、最後の整形と可視化直前の処理だけをデータフレームで受け取ります。「pandasのメモリ不足」問題の8割はこの分業で消えます。
拡張エコシステム
DuckDBはコアを小さく保ち、機能を拡張に分離しています。INSTALLとLOADの2コマンドで済みます。
-- 空間分析: GIS関数とジオメトリ型
INSTALL spatial;
LOAD spatial;
SELECT ST_Distance(
ST_Point(126.9780, 37.5665), -- ソウル
ST_Point(139.6917, 35.6895) -- 東京
) AS deg_distance;
-- 全文検索: BM25ベースのインデックス
INSTALL fts;
LOAD fts;
PRAGMA create_fts_index('docs', 'doc_id', 'body');
SELECT doc_id, fts_main_docs.match_bm25(doc_id, 'duckdb embedded') AS score
FROM docs ORDER BY score DESC LIMIT 10;
このほか、json(ほぼ組み込みレベル)、httpfs、iceberg、delta、postgresスキャナー、sqliteスキャナーがよく使われます。postgresスキャナーは本番DBのテーブルをDuckDBから直接読めるようにしてくれるため、「本番DBに負荷をかけずに分析レプリカを作る」手軽な代替手段になります。
データエンジニアリングにおける位置づけ — 軽量ETLとdbt
DuckDBは「アナリストのおもちゃ」を超えて、パイプラインの正式な部品になりました。代表的なパターンを2つ紹介します。
第一に、軽量ETLエンジンとしての利用です。1日数十GB以下のバッチであれば、Sparkクラスタの代わりにコンテナ1つでDuckDBによる変換を完結させる構成が、コストと運用難易度の両面で圧倒的にシンプルです。
# コンテナ1つで完結する日次バッチETLの骨格
import duckdb
con = duckdb.connect()
con.sql("INSTALL httpfs; LOAD httpfs;")
con.sql("""
COPY (
SELECT
cast(ts AS DATE) AS event_date,
user_id,
event_type,
amount
FROM 's3://raw-bucket/events/2026-06-11/*.jsonl'
WHERE event_type IN ('purchase', 'refund')
)
TO 's3://curated-bucket/events/dt=2026-06-11/data.parquet'
(FORMAT parquet, COMPRESSION zstd)
""")
第二に、dbt-duckdbです。dbtのモデル管理、テスト、ドキュメント化をそのまま使いながら、実行エンジンだけDuckDBにします。ウェアハウスのコストなしにSQL変換パイプラインのベストプラクティスを導入できるため、スタートアップやサイドプロジェクトでの採用が急速に増えました。CIでdbtモデルを丸ごと回すテスト環境としても卓越しています。
MotherDuck — ハイブリッド実行という折衷案
「ノートPCで足りなくなったらどうするか」への商業的な答えがMotherDuckです。DuckDBを作ったチームと協力するクラウドサービスで、ローカルのDuckDBとクラウドのDuckDBが1つのクエリの中で協調するハイブリッド実行を提供します。ローカルファイルとクラウドテーブルを結合すると、オプティマイザがどちら側でどの部分を実行するかを分けて処理する方式です。
-- ローカルCSVとMotherDuckクラウドテーブルの結合 (概念例)
ATTACH 'md:my_database';
SELECT l.campaign, m.user_segment, count(*)
FROM 'local_campaign.csv' l
JOIN my_database.users m USING (user_id)
GROUP BY 1, 2;
「組み込みで始めて、必要な分だけクラウドへ」という成長経路ができたわけです。ただしベンダー依存が再び生じるポイントでもあるため、コアパイプラインは標準のDuckDBとParquetに置き、MotherDuckは共有とスケール用途に限定するチームが多いです。
DuckLakeとレイクハウスの潮流
2025年、DuckDBチームはDuckLakeというオープンなレイクハウスフォーマットを公開しました。Iceberg系フォーマットがメタデータを多数のJSONとAvroファイルで管理するのに対し、DuckLakeはカタログメタデータをSQLデータベースに保存し、データはParquetに置くという単純化を選びました。「メタデータも結局はリレーショナルデータである」という発想です。まだ初期段階で、エコシステム互換性(特に既存のIcebergツールとの)が注目ポイントですが、単一ノードから始められるレイクハウスという方向性はDuckDBの哲学と一貫しています。既存のIcebergとDeltaテーブルはそれぞれ拡張で読めるため、当面は「読み取りは標準フォーマット、実験はDuckLake」くらいの距離感が適切でしょう。
日常レシピ集 — 手に馴染ませておきたいもの
DuckDBにはアナリストの繰り返し作業を減らす便利な構文が多くあります。よく使うことになるものを集めました。
-- 1. SUMMARIZE: テーブルプロファイリングを一発で
-- 各列のmin、max、近似ユニーク数、null比率まで自動要約
SUMMARIZE SELECT * FROM 'events.parquet';
-- 2. EXCLUDE / REPLACE: 列が多いときの救世主
SELECT * EXCLUDE (raw_payload, internal_flag)
FROM wide_table;
SELECT * REPLACE (round(amount, 2) AS amount)
FROM sales;
-- 3. 重複排除の定石: グループごとに最新1件だけ残す
SELECT * FROM events
QUALIFY row_number() OVER (
PARTITION BY user_id ORDER BY ts DESC
) = 1;
-- 4. 再現可能なサンプリング
SELECT * FROM big_table USING SAMPLE 1 PERCENT (bernoulli, 42);
-- 5. リスト集約と構造体: 非正規化データの取り扱い
SELECT user_id, list(distinct category) AS categories
FROM purchases GROUP BY user_id;
特にSUMMARIZEは初めて受け取ったデータの品質を1分以内に把握させてくれ、QUALIFYはウィンドウ関数の結果をサブクエリなしでフィルタリングさせてくれます。この2つが手に馴染むだけでも探索速度が目に見えて変わります。
もう1つ強調したいのは寛容なCSV処理です。区切り文字の推論、エンコーディング問題、壊れた行を無視するオプションまで、現実のデータの汚さをかなりうまく受け止めてくれます。
-- 壊れた行を捨てながら読み、どの行が捨てられたか確認
SELECT * FROM read_csv('messy.csv', ignore_errors = true);
SELECT * FROM read_csv('messy.csv', store_rejects = true);
SELECT * FROM reject_errors; -- 拒否された行と理由のテーブル
限界 — 何に使ってはいけないか
DuckDBの限界は長所の裏面です。
第一に、同時書き込みに弱いです。単一プロセス内ではマルチスレッドでうまく動作しますが、複数のプロセスが同じDBファイルに同時に書き込む構造はサポートされていません(読み取り専用の多重アクセスは可能)。多数のユーザーが同時に書き込むサービスのバックエンドDBには不向きです。
第二に、分散処理がありません。単一マシンのコアとディスクが限界です。数TBを超える定期バッチや、数百同時クエリのBIサービングはClickHouseやクラウドウェアハウスの領域です。ただしその境界が思ったより高いというのが核心です。最近の単一マシンはコア数十個、メモリ数百GBまで可能ですから。
第三に、長時間常駐するサービスよりもバッチと対話型分析に向いています。高可用性、レプリケーション、フェイルオーバーといった運用データベースの機能は、そもそも目標ではありません。
選択ガイド — Postgres、SQLite、ClickHouseとの比較
| 基準 | SQLite | DuckDB | Postgres | ClickHouse |
|---|---|---|---|---|
| デプロイ形態 | 組み込み | 組み込み | サーバー | サーバーまたは分散 |
| ストレージ方式 | 行指向 | 列指向 | 行指向 | 列指向 |
| 最適なワークロード | アプリ内OLTP | 単一ノードOLAP | 汎用OLTP | 大規模OLAPサービング |
| 同時書き込み | 単一ライター | 単一プロセスライター | 強力 | 強力 |
| 水平スケーリング | なし | なし | 限定的 | 中核的な強み |
| ファイル直接クエリ | 限定的 | 中核的な強み | 拡張が必要 | サポート |
| 運用負担 | ほぼなし | ほぼなし | 中程度 | 高い |
| 適した規模 | デバイスローカルデータ | GBから数百GB | サービス全般 | TB以上、高同時性 |
粗く要約するとこうなります。アプリケーションの状態保存はSQLiteかPostgres、1人または1パイプラインの分析はDuckDB、組織全体が叩くリアルタイム分析サービングはClickHouseです。そしてDuckDBは、残り3つすべてのデータを読める接着剤でもあります。
パフォーマンスのヒント
最後に、実務で効果の大きいチューニングポイントを整理します。
-- 1. メモリとスレッドを明示的に管理
SET memory_limit = '8GB';
SET threads = 8;
-- 2. 実行計画を確認する習慣
EXPLAIN ANALYZE
SELECT category, sum(amount) FROM 'sales/*.parquet' GROUP BY 1;
-- 3. CSVは一度Parquetに変換しておくと以降のすべてのクエリが速くなる
COPY (SELECT * FROM 'raw/*.csv') TO 'data.parquet'
(FORMAT parquet, COMPRESSION zstd);
-- 4. パーティションディレクトリ構造を活用したプルーニング
-- year=2026/month=06 のようなhiveパーティショニングを認識する
SELECT count(*) FROM read_parquet('events/*/*/*.parquet', hive_partitioning = true)
WHERE year = 2026 AND month = 6;
さらに3つ覚えておくと良いでしょう。第一に、SELECTのアスタリスクを避けて必要な列だけ指定すれば、列プルーニングの効果を完全に受けられます。第二に、大きなソートや結合がメモリを超えると自動的にディスクスピルが起こりますが、一時ディレクトリを高速なSSDに指定すると体感が違います。第三に、同じデータを繰り返しクエリするなら、CREATE TABLE ASでDuckDBネイティブフォーマットに載せておく方がParquetの再スキャンより高速です。
おわりに
DuckDBの成功は、華やかな新技術ではなく、正確なポジショニングの勝利です。ほとんどの分析データは1台のマシンに収まり、ほとんどのアナリストはサーバー運用を望まず、ほとんどのパイプラインはParquetファイルを扱います。DuckDBはこの3つの現実に、列指向ストレージとベクトル化実行という正統派データベース技術を正確に差し込みました。
導入の負担もありません。今日扱っていたCSVを1つduckdbシェルで開いて、GROUP BYを投げてみることから始めればよいのです。その一度の体験が「このデータ、本当にウェアハウスに載せる必要があるのか?」という健全な問いにつながるはずです。ノートPCで完結する分析の時代は、すでに来ています。
参考資料
- DuckDB公式ドキュメント: https://duckdb.org/docs/
- Why DuckDB (公式紹介): https://duckdb.org/why_duckdb
- DuckDB 1.0リリース発表: https://duckdb.org/2024/06/03/announcing-duckdb-100.html
- DuckLake発表記事: https://duckdb.org/2025/05/27/ducklake.html
- DuckDB GitHubリポジトリ: https://github.com/duckdb/duckdb
- dbt-duckdbアダプター: https://github.com/duckdb/dbt-duckdb
- MotherDuck公式サイト: https://motherduck.com/
- DuckDB Python APIドキュメント: https://duckdb.org/docs/api/python/overview
- httpfs拡張ドキュメント: https://duckdb.org/docs/extensions/httpfs/overview
- ASOF JOINドキュメント: https://duckdb.org/docs/sql/query_syntax/from
- Hacker NewsのDuckDB議論: https://news.ycombinator.com/item?id=24531085
- GeekNewsメイン: https://news.hada.io/