远程查询 Parquet 或 CSV 时,CPU 往往不是最先遇到的瓶颈。查询线程需要等待对象存储、HTTP 服务或网络文件系统返回数据,计算线程因此被大量空闲时间占据。DuckDB 从 v2.0 开始引入面向 Parquet 和 CSV 的异步读取架构,目标就是把等待 I/O 的时间交给专用线程处理,让计算线程持续进行解码、连接和聚合。
来源标题给出的结果是远程查询最高加速 20 倍。这个数字不能脱离数据分布、网络延迟、文件大小、谓词过滤和缓存命中率单独理解,但它准确指出了一个方向:远程分析的性能上限,越来越取决于 I/O 调度,而不只是 CPU 核数。
两类线程池分工
这次架构变化的核心是把计算和异步 I/O 分开。
REGULAR线程池负责 Parquet 解码、表连接、聚合等计算任务,线程数等于 CPU 核心数。ASYNC线程池负责发起和处理异步文件读取,默认线程数为系统线程数的 4 倍,上限为 256 个线程。- 预读队列会在计算线程真正需要数据之前,提前准备后续数据块。
传统同步读取通常接近这样的流程:
读取数据块 -> 等待网络 -> 解码数据块 -> 读取下一个数据块
异步读取则可以让不同阶段重叠:
ASYNC: 读取块 A ---- 读取块 B ---- 读取块 C
REGULAR: 解码块 A ---- 解码块 B ---- 聚合块 C
这并不意味着增加线程就一定能得到线性加速。远程服务的连接数限制、带宽、请求限流、对象存储吞吐和本地内存,都会成为新的边界。异步 I/O 的价值在于隐藏等待时间,而不是消除网络传输本身。
为什么远程 Parquet 更容易受益
Parquet 是列式格式。一个查询只读取少数列时,DuckDB 可以跳过不相关列,并结合行组统计信息执行谓词下推。例如,下面的查询只需要读取 event_time、region 和 amount:
SELECT
region,
sum(amount) AS total_amount
FROM read_parquet('https://example.com/data/events/*.parquet')
WHERE event_time >= DATE '2026-01-01'
AND region = 'apac'
GROUP BY region;
如果文件的行组统计信息可以排除大量数据,异步预读就能把有限的网络请求集中到真正可能命中的数据块上。计算线程处理当前块时,异步线程池可以准备下一批块,从而减少网络延迟对整体查询时间的影响。
CSV 的场景也类似,但 CSV 通常需要更多解析工作,类型推断和文本解码成本更高。因此,实际收益要同时看网络等待和解析开销。对于已经被本地缓存、数据量很小,或者查询必须扫描整个文件的情况,异步读取带来的收益可能明显降低。
一个可运行的远程查询基线
下面的 Python 示例用于建立查询基线。它依赖 DuckDB 的 HTTP 文件系统扩展,读取公开的远程 Parquet 文件。运行前请安装 Python 客户端:
python -m pip install duckdb
示例代码:
import time
import duckdb
URL = "https://raw.githubusercontent.com/duckdb/duckdb/main/data/csv/test.csv"
con = duckdb.connect()
con.execute("INSTALL httpfs")
con.execute("LOAD httpfs")
start = time.perf_counter()
result = con.execute(f"""
SELECT COUNT(*) AS rows
FROM read_csv_auto('{URL}')
""").fetchone()
elapsed = time.perf_counter() - start
print(f"rows={result[0]}")
print(f"elapsed_seconds={elapsed:.3f}")
这个例子展示的是远程文件查询方式,不代表当前稳定版本已经暴露了 v2.0 的异步线程池配置接口。异步读取功能进入目标版本后,可以用同一类查询对比升级前后的延迟、吞吐和请求数量。为了让结果有意义,建议固定以下条件:
- 使用同一批远程文件和同一查询语句。
- 分别测试冷缓存和热缓存。
- 记录总耗时、扫描字节数、CPU 利用率和网络吞吐。
- 测试单并发与多并发,观察远程服务是否开始限流。
可以用 EXPLAIN ANALYZE 查看查询计划和算子耗时:
plan = con.execute(f"""
EXPLAIN ANALYZE
SELECT COUNT(*)
FROM read_csv_auto('{URL}')
""").fetchall()
for row in plan:
print(row[1] if len(row) > 1 else row[0])
对于生产环境,建议将远程数据按时间、租户或业务分区组织,并尽量使用 Parquet。查询中只选择需要的列,过滤条件尽量直接作用于分区列或可利用统计信息的列。这样异步 I/O 才有机会把预读资源投入到有效数据上,而不是并行下载大量最终会被丢弃的内容。
调优时要注意的边界
ASYNC 线程池默认规模较大,最高可达到 256 个线程。高延迟对象存储环境可能从更多并发请求中受益,但本地部署、带宽有限或远程服务有严格限流时,线程过多会带来连接竞争、上下文切换和服务端错误率上升。
部署前可以按下面的顺序验证:
- 先用固定数据集测量同步基线。
- 确认查询确实受远程读取影响,而不是被 CPU 解码或聚合限制。
- 从较低并发开始,逐步增加异步读取并发。
- 同时监控 DuckDB 进程内存、文件描述符、网络带宽和远程服务的
429、5xx响应。 - 将结果与缓存策略、文件大小和分区布局一起评估。
异步 I/O 也不能替代数据布局优化。如果一个查询必须扫描数百个小文件,预读队列可能只是更快地发起大量低效请求。合并小文件、控制 Parquet 行组大小、避免不必要的 CSV 扫描,通常比盲目提高线程数更稳定。
采用建议
DuckDB v2.0 的异步读取适合重点关注远程分析、数据湖查询和对象存储访问的团队。升级时不要只比较一条查询的墙钟时间,而应建立包含冷缓存、热缓存、不同并发度和不同选择率的基准集。
可以把这项能力理解为一次调度层升级:REGULAR 线程池专注于把数据算出来,ASYNC 线程池专注于让数据及时到达,预读队列负责把两者连接起来。真正的收益取决于查询是否能够减少无效扫描,以及远程数据源是否允许足够的并发。