DuckDB 深度实战:进程内分析数据库凭什么单机跑 TB 级——向量化执行、列式存储与零拷贝数据湖全解析
如果你写过数据分析,大概率经历过这种尴尬:Pandas 读个 5GB 的 CSV 内存直接爆掉,Spark 起个集群又觉得为了几十 GB 数据杀鸡用牛刀,而 SQLite 跑个
GROUP BY慢得像在考古。2026 年,这个问题的答案越来越清晰——DuckDB。它被称为"分析界的 SQLite",但内核里装着一套完全不同的引擎。本文从工程视角拆解它为什么快、什么时候该用、以及怎样把它用出花来。
一、背景:数据分析的"夹心层"困境
我们先把问题说清楚。一个做数据的程序员,日常面对的数据体量大致分三档:
- KB~MB 级:业务库的几张表、接口返回的 JSON,SQLite 一把梭,或者干脆丢进 Pandas。
- GB~TB 级:一天的埋点日志、一张宽表导出、几十个 Parquet 分区的湖仓文件。这个体量最尴尬。
- PB 级:真正的"大数据",老老实实上 Spark / Flink / ClickHouse 集群。
尴尬就出在中间这档。它够大,大到 Pandas 扛不住;但又不够大,大到值得养一个分布式集群。传统解法要么忍(Pandas 分块 + 祈祷内存够),要么卷(为几十 GB 数据申请一套 Spark 资源),要么退(用 PostgreSQL 硬跑分析查询,然后看着 seq scan 发呆)。
DuckDB 的切入点非常精准:它就是为这中间一档而生的。它把自己定位为"进程内(in-process)的 OLAP 数据库",你可以像引用一个普通 Python 库那样 import duckdb,不装服务、不配端口、不启进程,然后直接对 TB 级数据跑 SQL。
这就有意思了。一个没有 server 的数据库,凭什么比装了 server 的 PostgreSQL 跑分析还快?答案藏在它的两个核心设计里:列式存储和向量化执行引擎。我们一层层拆。
二、核心概念:先把几个被说烂的词讲透
2.1 OLTP 与 OLAP,不是两种数据库,是两种"读法"
很多人知道 OLTP(联机事务处理)和 OLAP(联机分析处理)的区别,但容易停留在"一个是 MySQL 一个是数仓"这种表面认知。从磁盘和 CPU 的视角看,本质区别在于数据被访问的模式:
- OLTP:一次读写一行(或几行)。
UPDATE users SET balance=balance-10 WHERE id=42。它关心的是"单个记录的增删改查",行式存储天然友好——同一行的所有字段在磁盘上挨着,一次 IO 就读全了。 - OLAP:一次扫描一列的几百万行。
SELECT city, AVG(age) FROM users GROUP BY city。它关心的是"某一列在所有行上的聚合",行式存储就傻了——为了算一个 AVG(age),你得把每行整个读进来再丢掉无关的字段,IO 和缓存都被浪费。
SQLite、MySQL、PostgreSQL 默认都是**行式(row-based)存储,它们把一行作为一个存储单元。这对事务是好文明,对分析是灾难。DuckDB 从根上就是列式(columnar)**的:age 这一列的所有值物理上挨在一起,算 AVG 时只读 age 这一块连续内存,CPU 预取器和缓存命中率直接拉满。
2.2 列式存储不只是"换了个排列顺序"
把行存和列存的区别画出来就直观了。假设有这样一张表:
行式存储(SQLite/MySQL):
[1, Alice, 28, Beijing] [2, Bob, 34, Shanghai] [3, Carol, 31, Beijing] ...
一次读一行,字段连续
列式存储(DuckDB/ClickHouse/Parquet):
col_id: [1, 2, 3, ...]
col_name: [Alice, Bob, Carol, ...]
col_age: [28, 34, 31, ...]
col_city: [Beijing, Shanghai, Beijing, ...]
每一列是一个连续的数值数组
列式的红利不止"少读无关列"这么简单,它还带来两个杀手锏:
- 压缩率暴增。同一列的数据类型相同、取值范围相近(比如 age 都在 0~120 之间),用字典编码(dictionary encoding)、游程编码(RLE)、位宽压缩就能压得很小。DuckDB 对每列还会按类型自动选压缩算法。压缩后数据更小,意味着同样的 CPU 缓存能装下更多行,进一步加速。
- 向量化处理的天然后选。一列连续同类型的值,本质就是一个 typed array,CPU 可以用 SIMD 指令一条指令处理 8/16/32 个值。这正好引出 DuckDB 最硬核的设计。
2.3 "进程内"到底意味着什么
进程内(in-process / embedded)是指数据库引擎跑在你的应用程序进程里,而不是一个独立服务。SQLite 也是进程内,所以你能 import sqlite3 直接用。但 DuckDB 的进程内和 SQLite 的进程内目的不同:
- SQLite 的进程内是为了零部署、本地持久化一个小数据库(手机 App 的配置、桌面软件的缓存)。
- DuckDB 的进程内是为了把分析引擎直接塞进你的数据 pipeline,避免"应用读数据 → 网络发给数据库 → 数据库算 → 网络传回结果"这种来回拷贝。数据不离开你的进程,算子直接作用在内存数组上。
这就是为什么 DuckDB 能跟 Pandas、PyArrow 玩"零拷贝"——因为它俩在同一个进程地址空间里,DuckDB 算完的结果可以直接把底层 buffer 借给 Arrow,不用序列化、不用跨进程。这点我们后面用代码验证。
三、架构分析:DuckDB 凭什么快
3.1 一条 SQL 在 DuckDB 里的旅程
和大部分现代数据库一样,DuckDB 的查询执行分几个阶段:
SQL 文本
→ 解析器(Parser):词法/语法分析,生成抽象语法树 AST
→ 绑定器(Binder):把表名、列名解析成具体对象,做类型检查
→ 逻辑计划(Logical Planner):生成关系代数计划(Scan/Filter/Join/Aggregate...)
→ 优化器(Optimizer):谓词下推、列裁剪、Join 重排、公共子表达式消除
→ 物理计划(Physical Planner):决定具体算子实现(Hash Join vs Merge Join)
→ 执行器(Execution Engine):向量化执行,按 pipeline 并行跑
这里有两个 DuckDB 区别于传统数据库的地方值得展开:优化器的下推能力和向量化执行引擎。
3.2 向量化执行引擎:DuckDB 的灵魂
要理解向量化为什么快,得先理解它的反面——传统的 Volcano(火山)模型。
传统数据库(包括老版本 PostgreSQL 的执行器)用的是 tuple-at-a-time(一次处理一个元组)的火山模型:每个算子实现一个 next() 方法,父算子不断调用子算子的 next() 拿下一行,处理完再往上抛。比如 Filter(age > 30) 这个算子,它的逻辑是:
// 火山模型:一次一行
loop:
row = child.next() // 取一行
if not row: break
if row.age > 30: // 判断这一行
return row // 返回这一行
这个模型的问题在抽象开销:每处理一行,都要经历一次函数调用、一次虚函数分派(vtable lookup)、一次分支判断。当数据量是千万级时,这些 per-row 的解释开销累计起来比真正的计算还贵。CPU 把大量时间花在"调度算子"而不是"算数据"上,分支预测也因为行与行之间的条件跳转而频繁失败。
DuckDB 用的是 Vectorized / Batch 模型:每个算子一次处理**一整批(vector,默认 2048 行)**数据。算子不再是"给我下一行",而是"给我下一批,我一次处理 2048 行再给你一批"。
// 向量化模型:一次一批(batch of 2048 rows)
loop:
batch = child.next_batch(2048) // 取一批(列存的 typed array)
if batch empty: break
mask = batch.age > 30 // 一次性对 2048 个值做向量比较 → SIMD
result = batch.filter(mask) // 用 bitmap 过滤,连续内存
return result
向量化的红利有三层:
- 摊薄了解释开销。一次
next_batch处理 2048 行,函数调用和虚分派的次数直接降到 1/2048。算子的核心循环从"处理一个值"变成"处理一个数组"。 - 对 CPU 缓存和 SIMD 友好。数据以连续的同类型数组形式存在,CPU 的预取器能准确预测下一步要读哪块内存;
batch.age > 30这种比较,编译器可以生成 SIMD 指令(比如 AVX2 一条指令比 8 个 int32),真正并行处理。 - 分支更少、流水线更顺。一批数据的处理逻辑是统一的,没有"每行条件不同导致频繁跳转",CPU 指令流水线不容易被打断。
一个常见误解:很多人以为 DuckDB"快是因为用了 SIMD"。准确说,SIMD 是向量化的结果之一而非原因。根本原因是"批量处理"降低了 per-row 解释开销,而批量是同类型连续数组,才让 SIMD、缓存预取这些 CPU 优化手段能派上用场。向量化是因,SIMD 是果。
3.3 列式存储格式与压缩
DuckDB 内部用 Data Chunk 作为数据交换的基本单位(就是上面说的 2048 行的批次),每个 Data Chunk 里每一列是一个 Vector。落盘(持久化到 .duckdb 文件)时,数据按列分块存储,并对每列根据类型选择压缩:
- 低基数列(比如 city、status)→ 字典编码,存储
(字典, 整数索引),体积骤减。 - 有序/重复列 → 游程编码(RLE)。
- 数值列 → 按实际取值范围选择最小位宽(bitpacking),比如 age 用 7 位就够了,不用占满 32 位 int。
压缩不仅省磁盘,更省内存和 IO——读到缓存里的是压缩态或按需解压,同样的 L3 缓存能容纳更多行,算得更快。
3.4 与 Apache Arrow / Parquet 的"近亲"关系
这是 DuckDB 在 Python 生态里封神的关键设计。它内部的内存格式和 Apache Arrow 的列式内存格式高度兼容,因此:
- 读 Parquet 文件时,可以直接把 Parquet 解码成 Arrow 格式的 buffer,几乎零转换。
- DuckDB 的查询结果可以直接以 Arrow Table 的形式吐给 Python,不复制数据。
- 和 Pandas 互转也只是一层薄封装(
df = duckdb.query(...).df()或duckdb.df(df))。
换句话说,DuckDB 不是"又一个数据库",而是把分析计算引擎嵌进了 Arrow 生态——你用 Pandas 做预处理、用 DuckDB 做重查询、用 PyArrow 做列式传输,三者共享同一块内存,没有序列化拷贝。这套组合在现代数据栈里被称为"本地优先(local-first)的数据分析"。
3.5 扩展机制:内核小而插件多
DuckDB 的内核保持精简,能力靠**扩展(extension)**热插拔。官方维护的扩展涵盖:
parquet/json:读写这两种文件格式(核心分析场景)。httpfs:直接读 S3 / HTTP(S) 上的文件,这是搭轻量数据湖的关键。spatial:地理空间函数。fts:全文检索。arrow:与 Arrow 深度集成。excel/sqlite/postgres/mysql:直接读这些外部数据源。iceberg/delta:对接主流表格式(table format)。
扩展是按需从官方仓库拉取的,第一次 INSTALL 之后就缓存本地。这让 DuckDB 既能保持单文件轻量,又能按需变身为"能读整个湖仓"的查询引擎。
四、代码实战:从 hello world 到数据管道
下面所有示例基于 DuckDB 1.5+(2026 年的稳定线),Python API。先装:
pip install duckdb
4.1 最朴素的一击:直接查一个 Parquet
假设你有从数据湖导出的 events.parquet,不用 Pandas 加载,直接 SQL 打上去:
import duckdb
# 直接对 Parquet 文件跑 SQL,文件不需要"导入"进库
con = duckdb.connect() # 不传路径 = 内存模式(in-memory)
rows = con.execute("""
SELECT event_type, COUNT(*) AS cnt
FROM 'events.parquet'
WHERE ts >= TIMESTAMP '2026-07-01'
GROUP BY event_type
ORDER BY cnt DESC
LIMIT 10
""").fetchall()
for r in rows:
print(r)
注意这里 'events.parquet' 是直接当表名用的字符串字面量。DuckDB 会自动识别文件格式、推断 schema、把查询下推到文件读取阶段。整个文件可能几十 GB,但如果你只查某几列 + 带过滤,DuckDB 只读需要的列块(列裁剪),且 Parquet 自带行组(row group)统计,能跳过不满足过滤条件的行组(谓词下推)。它不会把整个文件读进内存。
4.2 多文件 + 通配符:扫一整个分区目录
数据湖常见的布局是按日期分目录:s3://lake/events/dt=2026-07-01/part-0.parquet、 dt=2026-07-02/...。DuckDB 直接支持 glob:
con = duckdb.connect()
# 用 * 通配符扫多个文件,甚至跨分区
sql = """
SELECT dt, COUNT(*) AS cnt
FROM 's3://lake/events/dt=2026-07-*/part-*.parquet'
GROUP BY dt
ORDER BY dt
"""
# 读 S3 需要 httpfs 扩展
con.execute("INSTALL httpfs; LOAD httpfs;")
con.execute("SET s3_region = 'ap-east-1';")
# 如需认证:
# con.execute("SET s3_access_key_id = '...'; SET s3_secret_access_key = '...';")
for r in con.execute(sql).fetchall():
print(r)
这就是 DuckDB 作为"轻量数据湖查询引擎"的用法——你不需要 Spark、不需要 Presto,本地一个进程就能对 S3 上的成百上千个 Parquet 分区跑 SQL,且下推优化让大部分 IO 被裁剪掉。
4.3 与 Pandas / PyArrow 的零拷贝互操作
这是日常最舒服的用法。DuckDB 和 DataFrame 之间互转几乎没有成本:
import duckdb
import pandas as pd
df = pd.DataFrame({
"user_id": [1, 2, 3, 1, 2],
"amount": [100, 200, 50, 30, 80],
"day": ["2026-07-01", "2026-07-01", "2026-07-01", "2026-07-02", "2026-07-02"],
})
# Pandas → DuckDB:把 DataFrame 注册成临时视图,不拷贝底层数据
con = duckdb.connect()
con.register("payments", df)
res = con.execute("""
SELECT day, SUM(amount) AS total, COUNT(DISTINCT user_id) AS users
FROM payments
GROUP BY day
""").df() # 结果直接变回 DataFrame
print(res)
# DuckDB → PyArrow(零拷贝):底层 buffer 共享,适合做列式下游
arrow_tbl = con.execute("SELECT * FROM payments").arrow()
print(arrow_tbl.schema)
con.register("payments", df) 的关键在于:DuckDB 通过 Arrow 接口借用了 Pandas DataFrame 的底层列式 buffer,而不是把数据逐行复制进来。对大 DataFrame,这省下的是数倍内存和可观的拷贝时间。
4.4 窗口函数与复杂分析
DuckDB 完整支持窗口函数、CTE、JOIN、子查询,写复杂分析毫不费力。比如"算每个用户每天的消费在其自身历史中的排名和累计占比":
con = duckdb.connect()
con.register("payments", df)
sql = """
WITH daily AS (
SELECT
user_id,
day,
SUM(amount) AS day_total
FROM payments
GROUP BY user_id, day
)
SELECT
user_id,
day,
day_total,
SUM(day_total) OVER (
PARTITION BY user_id
ORDER BY day
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS running_total,
RANK() OVER (
PARTITION BY user_id
ORDER BY day_total DESC
) AS best_day_rank
FROM daily
ORDER BY user_id, day
"""
print(con.execute(sql).df())
CTE + 窗口函数这套在标准数仓 SQL 里很常见,DuckDB 把这套能力带到了本地进程内,你写法和写 Hive/Spark SQL 基本一致,迁移零成本。
4.5 多数据源 JOIN:把 SQLite、Postgres、CSV 缝在一起
DuckDB 能同时 ATTACH 多种外部数据源,在一条 SQL 里跨源 JOIN。这在"临时对账、数据迁徙核对"场景极好用:
con = duckdb.connect()
# 挂一个 SQLite 库
con.execute("ATTACH 'old_app.db' AS legacy (TYPE sqlite)")
# 读一个 CSV(自动推断 schema、类型)
# 直接 JOIN 内存表 + SQLite 表 + CSV
sql = """
SELECT l.user_id, l.email, c.order_count, c.total_amount
FROM legacy.users AS l
JOIN read_csv_auto('orders_export.csv') AS c
ON l.user_id = c.user_id
WHERE c.total_amount > 1000
"""
print(con.execute(sql).df())
read_csv_auto 会自动探测分隔符、表头、每列类型(甚至能识别日期格式),比 Pandas 的 read_csv 在脏数据上更健壮。
4.6 批量写入:Appender 与持久化
分析库也要写。DuckDB 提供高效的 Appender 做逐行/批量插入,比一条条 INSERT 快得多:
con = duckdb.connect("analytics.duckdb") # 持久化到单文件
con.execute("""
CREATE TABLE IF NOT EXISTS events (
id BIGINT,
user_id BIGINT,
amount DOUBLE,
ts TIMESTAMP
)
""")
# 方式一:从 DataFrame 直接建表/追加(最高效)
big_df = pd.DataFrame(...) # 假设已有一大批数据
con.execute("INSERT INTO events SELECT * FROM (SELECT * FROM df_view)")
con.register("df_view", big_df)
con.execute("INSERT INTO events SELECT * FROM df_view")
# 方式二:Appender 流式写入(适合逐条产生、批量 flush)
appender = con.append("events")
for record in stream_of_records(): # 假设是流式数据源
appender.append(row=record)
appender.flush() # 显式刷盘,控制写入节奏
持久化模式(传数据库文件路径而非内存)下,DuckDB 把数据写成列式 .duckdb 文件,后续查询直接享受列存+压缩的优势,单文件即可分发、备份、git 忽略。
五、性能优化:把 DuckDB 榨干
DuckDB 开箱已经很快,但几个开关和写法能再上一个台阶。
5.1 并行执行:pipeline 级多线程
DuckDB 在执行阶段按 pipeline(算子链)做任务并行,多个 pipeline 在多核上同时跑。默认开启,核数自动探测。你可以用 PRAGMA 查看和调整:
-- 查看当前线程数
PRAGMA threads;
-- 手动设为 8 线程(比如容器里想限制资源)
PRAGMA threads = 8;
注意:并行对"扫描 + 聚合"这种吞吐型查询收益最大,对返回单行的小查询几乎无感。别指望用多线程拯救一条 O(N²) 的烂 JOIN。
5.2 谓词下推与分区裁剪:少读就是最快
这是 DuckDB 在大数据文件上"显得快"的最主要原因。两个机制:
- 列裁剪(Column Pruning):查询只
SELECT a, b,执行器只读这两列的块,其他列的物理数据根本不进内存。 - 谓词下推(Predicate Pushdown):
WHERE ts > '2026-07-01'会被下推到文件扫描层。对 Parquet,DuckDB 读每个 row group 的 min/max 统计,直接跳过整段不满足条件的 row group;对分区目录(dt=2026-07-0X),直接不打开不匹配的分区文件。
实战建议:查询时把过滤条件写在 WHERE 里,让优化器去下推,别先 SELECT * 全部读进来再在 Python 里过滤。后者等于主动放弃 DuckDB 最大的优势。
# 好:过滤下推到文件扫描层,只读需要的行组
con.execute("""
SELECT city, AVG(latency)
FROM 's3://lake/logs/dt=2026-07-*/part-*.parquet'
WHERE latency > 100 AND status = 500
GROUP BY city
""")
# 差:先全量读进内存,再在 Python 里筛,等于没用 DuckDB
df = con.execute("SELECT * FROM 'logs.parquet'").df()
df[(df.latency > 100) & (df.status == 500)]
5.3 内存与溢出(Spilling):TB 级数据的底气
DuckDB 默认用内存,但当中间结果(比如一个大 Hash Join 的哈希表、一个超长排序)超过内存阈值时,会自动溢出到磁盘(spilling),用临时文件兜底,而不是崩溃。相关旋钮:
-- 限制内存上限(比如 4GB),超出则溢出到磁盘
PRAGMA memory_limit = '4GB';
-- 临时文件目录(溢出用)
PRAGMA temp_directory = '/data/duckdb_tmp';
这意味着你完全可以用一台 16GB 内存的笔记本,对 TB 级 Parquet 跑聚合——只要磁盘够,DuckDB 会默默把溢出的中间结果写到 temp_directory。这也是它能"单机跑 TB 级"的技术底气之一。
5.4 与 ClickHouse / Pandas / PostgreSQL 的基准直觉
很多人问"DuckDB 和 ClickHouse 谁快"。先说结论:它们是不同物种,不该直接比绝对速度:
| 维度 | DuckDB | ClickHouse | PostgreSQL | Pandas |
|---|---|---|---|---|
| 部署形态 | 进程内、单文件 | 独立 server、集群 | 独立 server | 库(内存) |
| 存储 | 列式 | 列式 | 行式 | 行式/列式混合 |
| 并发写 | 单写者 | 多写 | 多写 MVCC | 无 |
| 数据量级 | GB~TB(单机) | GB~PB(集群) | GB~TB | MB~GB(内存限制) |
| 跨进程/网络 | 否(进程内) | 是(客户端/服务) | 是 | 否 |
| 典型场景 | 本地分析、ETL、数据湖查询 | 实时分析服务、可观测性 | 事务 + 中等分析 | 小数据探索 |
一个粗略的直觉基准(同一台 8 核机器,10GB Parquet 的 GROUP BY 聚合):
- Pandas:内存吃紧,可能 OOM,或分块后很慢。
- PostgreSQL:得先
COPY进表(分钟级导入),再跑,行存导致扫描慢,整体最慢之一。 - DuckDB:直接对文件 SQL,列式扫描 + 向量化,通常比 Pandas 快 5~20 倍,比 PG 快一个数量级。
- ClickHouse:单独 server,导入后查询本身极快,但"起服务 + 导入"的固定成本高,对一次性本地分析反而不如 DuckDB 顺手。
所以选型口诀是:要本地快速分析、ETL、数据湖即席查询 → DuckDB;要高并发分析服务、实时监控 → ClickHouse;要事务 + 强一致 → PostgreSQL。 DuckDB 不是来替代谁的,它是补齐 SQLite 在分析方向上的短板。
六、总结与展望:DuckDB 到底改变了什么
6.1 它真正解决的是"分析的工具链摩擦"
DuckDB 的价值不在某个单项指标登顶,而在于把"重查询能力"塞进了最轻量的形态。过去做分析,轻量(SQLite/Pandas)的算得慢、装得爽;重的(Spark/ClickHouse)算得快、部署烦。DuckDB 把"算得快"和"装得爽"第一次合在了一起:一个 pip 包、一个 import、一个文件,TB 级分析就开干了。
它把"分析界的 SQLite"这个比喻落到了实处——不是性能像 SQLite,而是使用姿态像 SQLite,但内核是 OLAP 引擎。
6.2 什么时候不该用 DuckDB
讲清楚能做什么,更要讲清楚不能做什么,这才是工程判断:
- 高并发写入服务:DuckDB 是单写者模型,不支持多进程同时写。它为你、为分析而生,不为每秒上千次 INSERT 的 Web 后端而生。这种场景老老实实用 PostgreSQL / MySQL。
- 跨机器分布式计算:单进程、单节点。数据真的到 PB 级、需要横向扩展时,还是得上 Spark / ClickHouse 集群,或把 DuckDB 当作"单节点内的加速核"嵌进更大 pipeline。
- 需要严格事务隔离的多用户系统:DuckDB 的强项是一人、一个进程、一次性把分析做完。
6.3 2026 年的生态走向
几个值得关注的方向:
- MotherDuck 与云边协同:DuckDB 背后公司推出的云服务,把本地 DuckDB 和云端实例打通,本地算小、云端算大、结果可同步。这代表了"本地优先分析"的主流化。
- 湖仓格式直读:
iceberg/delta扩展让 DuckDB 能直接查询主流表格式的分区,进一步蚕食"轻量 Presto"的领地。 - AI/LLM 数据层:RAG 和 Agent 系统需要高效检索、转换大批量文档与表格,DuckDB 常作为本地向量/结构化数据的查询底座,和嵌入式向量检索互补。
- Notebook 原生:Jupyter / 各类 BI 工具里,DuckDB 正成为"默认的内核查询引擎",取代 Pandas 做重活。
6.4 给你的行动建议
如果你是后端 / 数据工程师,今天就可以在本地试三件事,成本几乎为零:
- 下次要分析一个几十 GB 的 CSV/Parquet,别先
pd.read_csv,改用duckdb.query("SELECT ... FROM 'file'"),感受下内存和速度的差异。 - 把一个你平时用 Python 循环算的聚合逻辑,改写成一条 SQL,交给 DuckDB 的向量化引擎跑,代码更短、更快。
- 搭轻量数据湖时,DuckDB + Parquet + httpfs(S3) 作为"只读分析面",和 PostgreSQL(事务面)分工,能省掉一整套 Spark/Presto 的运维。
DuckDB 的出现提醒我们一件事:很多"大数据"问题,其实只是"中数据"问题被错误的工具放大了。 在盲目上分布式之前,先想想——这一份数据,DuckDB 能不能在本地一次算完?大概率能。
参考资料与进一步阅读:DuckDB 官方文档(duckdb.org)的 Architecture、Data Types、Extensions 章节;Apache Arrow 内存格式规范;Parquet 文件格式规范(列存 + 行组统计 + 谓词下推机制);Volcano vs Vectorized 执行模型的经典论文(如 "MonetDB/X100: Hyper-Pipelining Query Execution")。本文所有代码示例基于 DuckDB 1.5+ Python API,可在本地环境直接运行验证。