SQLite が「アプリに埋め込む小さなトランザクションDB」なら、DuckDBは「アプリに埋め込む分析(OLAP)用DB」だ。サーバーを起動せず、ライブラリとしてプロセス内で動く。PythonやRから1行で呼べて、CSVやParquet、果てはPandas/NumPyの配列をコピーせずそのままクエリできる。iPhone上でTPC-H(標準ベンチ)のSF100を回し、単体でクラウドDBに並ぶ速度を出した、という逸話もある。
普通のDBは1行ぶんのデータを連続して並べる(行指向)。「IDも名前も年齢も住所も…」と1人分が固まっている。一方DuckDBは同じ列の値をまとめて並べる(列指向)。300列のテーブルから4列だけ集計するとき、行指向は全300列を読み込まざるを得ないが、列指向はその4列だけを読む。分析は「全行の一部の列を舐める」処理が大半だから、これだけで桁違いに無駄が減る。
さらに各列を最大122,880行の行グループに区切り、グループごとに最小値・最大値(ゾーンマップ)を持つ。`WHERE age > 80` のような条件なら、最大値が80以下のグループは読まずに丸ごと飛ばせる。
1行ずつ関数を呼んで処理すると、関数呼び出しや分岐のオーバーヘッドが1行ごとに乗る。DuckDBは1024行などのかたまり(ベクトル)単位で処理を流す。CPUのキャッシュとSIMD(同じ演算を複数データに一度にかける命令)が効きやすく、1行あたりの“事務コスト”がならされる。工場のベルトコンベアで部品をまとめて流すイメージだ。
クエリの物理プランは、パイプライン(フィルタや射影のように状態を持たず流せる処理の連鎖)と、シンク(GROUP BYやソート、ハッシュ結合の構築側のように全入力を見てから次へ渡す“せき止め”)の組み合わせになる。パイプラインの中では入力をmorsel(小さな断片)に割り、複数スレッドが互いにロックを取らず独立に処理する。シンクでは「各スレッドが自分のローカル状態に書く→並列にマージ→確定」の3段で合流する。全体を一枚岩で並列化するのではなく、局所ごとにロックを最小化して並べるのがDuckDBの設計判断だ。
従来のクライアント/サーバDBは、結果をネットワーク越しに行単位・フィールド単位で受け渡す。2017年の論文「Don't Hold My Data Hostage」は、この転送プロトコル自体がボトルネックになり、ギガビット回線でも“送る時間 > 計算する時間”になる場合を示した。DuckDBはインプロセスかArrow形式の列バッファで渡すので、この運搬コストがほぼ消える(zero-copy)。Parquetや遠隔ファイルでも、フッターの統計を見て必要なバイトだけ取りに行く。