KernelRanch
返回文章列表

同一份 200 万行 CSV:DuckDB、PostgreSQL、SQLite 本地分析查询实测

2026-11-10 #数据库

做本地数据分析时,SQLite 是默认选项,PostgreSQL 是"正经数据库",DuckDB 是这两年才进入视野的第三种。三者差别到底有多大?我用同一份 CSV 在 Windows 上跑了一遍,结论比预期更极端:DuckDB 比 PostgreSQL 快一个数量级,而 PostgreSQL 又比 SQLite 快一个数量级还多。

0. 方法与环境

项值
机器Windows 11,32 vCPU / 32 GiB,Docker Desktop 29.4.3
数据2,000,000 行 × 5 列(id, user_id, ts, amount, status),CSV 93.6MB,固定随机种子 42
DuckDB2.0.0(Python 包,进程内)
SQLite3.45.1(Python 标准库)
PostgreSQL18.6(Docker,shared_buffers=256MB,其余默认)
计时每条查询连续跑 3 次取中位数;PostgreSQL 用 \timing 的 Time:,另外两家用 time.time()
索引SQLite 与 PostgreSQL 都在 ts 上建了普通索引,DuckDB 不建(列存不需要)

三条查询分别代表:低基数分组聚合(q1)、时间分桶(q2)、高基数分组 + Top-N(q3)。

-- q1
SELECT status, count(*), sum(amount) FROM events GROUP BY status;
-- q2(PostgreSQL/DuckDB 用 date_trunc,SQLite 用 substr(ts,1,10))
SELECT date_trunc('day', ts) d, count(*) FROM events GROUP BY d ORDER BY d;
-- q3
SELECT user_id, sum(amount) s FROM events GROUP BY user_id ORDER BY s DESC LIMIT 20;

1. 结果

引擎导入耗时落盘体积q1q2q3
DuckDB 2.0.00.2 s21.5 MB6 ms9 ms27 ms
PostgreSQL 18.61.7 s164 MB110 ms96 ms412 ms
SQLite 3.45.13.0 s156 MB766 ms730 ms905 ms

几个直接能读出来的事实:

  • DuckDB 的聚合查询比 PostgreSQL 默认配置快 18 倍左右,比 SQLite 快 100 倍以上。
  • 落盘体积:DuckDB 21.5MB,只有另外两家的 1/7。列存 + 压缩,对分析型数据几乎是白送的。
  • 导入耗时 DuckDB 最短(0.2s 直接把 CSV 读成表),SQLite 最慢(3.0s,逐行 insert)。

2. 差距从哪来

DuckDB 官方文档对这件事说得很直接:它使用"列式向量化执行引擎",而"PostgreSQL、MySQL、SQLite 这类传统系统逐行顺序处理",因此"向量化执行在 OLAP 查询上带来远好得多的性能"[1]。这段话解释了三家的分工:

  • SQLite 是 B-tree 行存,我的 q1 要扫 200 万行的 status 与 amount 两列,行存意味着要把无关的三列也读进来。
  • PostgreSQL 也是行存,但它有并行聚合和更好的哈希聚合实现,所以比 SQLite 快 7 倍左右。
  • DuckDB 是列存 + 向量化,且进程内运行,没有客户端/服务端序列化开销。

顺带说,DuckDB 文档还提到它在目标场景下"进程内运行带来数据进出的高速传输"[1]——这一点在 Python 里很实在:数据可以直接从 Pandas 读,不需要先导出再导入。

3. 公平性检验:把并行度拉平

上面的 PostgreSQL 用的是默认 max_parallel_workers_per_gather = 2。这不公平,所以又补了两组:

配置q1 耗时
PostgreSQL 默认(并行 2)110 ms
PostgreSQL max_parallel_workers_per_gather=870 ms(实际计划里 Workers Planned: 3)
DuckDB 多线程(默认)6 ms
DuckDB PRAGMA threads=114.6 ms

即使把 PostgreSQL 的并行拉到 8、把 DuckDB 压到单线程,DuckDB 仍然快 5 倍左右(14.6 vs 70)。所以差距不是"谁用的核多",而是执行模型不同:逐行解释 vs 向量化批处理。

这里还有个容易误读的细节:我设了 8 个并行 worker,EXPLAIN 里显示的是 Workers Planned: 3。原因是 max_parallel_workers(默认 8)要分给所有并行查询,加上表大小与代价估算,实际不一定按你设的上限来。调并行度不能只看 GUC 数值,要看计划里的 Workers Planned。

4. 什么时候不该用 DuckDB

跑分好看不等于能替换。三个边界要说清楚:

并发写。 DuckDB 是进程内嵌的,定位是分析型负载。多个进程同时写同一个库文件,不是它的设计目标。官方的定位描述里,OLAP 工作负载的特征是"复杂、相对长跑的查询,处理数据集的相当大一部分",数据变更"期望是大规模的、成批追加或大块替换"[1]。你要的是每秒几百个短事务,那是 PostgreSQL 的活。

多客户端与服务端语义。 PostgreSQL 有连接、权限、行级锁、约束、触发器、逻辑复制,这些是"多人共用一份数据"的基础设施。DuckDB 进程内运行,天然没有这些。

SQLite 也不是不能做分析。 官方文档专门列了"Data analysis"场景,认为用 SQL 处理 CSV、出汇总报告是合适用法[2]。它慢的原因是执行模型(逐行 B-tree),不是能力缺失。数据量在几十万行、查询简单时,SQLite 的 766ms 完全够用,而且零依赖——Python 自带。

选型上我自己的判断标准是这样:

场景选择
本地一次性分析、几十万到上亿行、复杂聚合DuckDB
多客户端读写、需要事务与约束、线上服务PostgreSQL
单机小工具、嵌入式、单写者、要单文件分发SQLite
需要同时支持"分析"和"多人写"PostgreSQL 存明细 + DuckDB 做分析副本

5. 在 Windows 上复现

# DuckDB(进程内,装个包就行)
python -m pip install duckdb

# SQLite 用标准库,无需安装
python -c "import sqlite3; print(sqlite3.sqlite_version)"

# PostgreSQL 用容器(不要映射宿主端口,用 docker exec 连)
docker run -d --name pgcmp --memory 1g -e POSTGRES_PASSWORD=postgres postgres:18 -c shared_buffers=256MB
docker cp events.csv pgcmp:/tmp/events.csv
docker exec -u postgres pgcmp psql -c "create table events(id int, user_id int, ts timestamp, amount numeric(10,2), status text);"
docker exec -u postgres pgcmp psql -c "\copy events from '/tmp/events.csv' csv header"

DuckDB 侧最省事的写法是直接读 CSV,不建库:

import duckdb
con = duckdb.connect("events.duckdb")
con.execute("create table events as select * from read_csv_auto('events.csv', header=true)")
con.execute("select status, count(*), sum(amount) from events group by status").fetchall()

小结

同一份 93.6MB 的 CSV,三条聚合查询,DuckDB 6–27ms,PostgreSQL 96–412ms,SQLite 730–905ms。把 PostgreSQL 并行拉到 8、DuckDB 压到单线程,差距仍有 5 倍。

这不是"谁更好"的问题。DuckDB 的列式向量化引擎天然适合"扫一大片、算一个数",PostgreSQL 的价值在并发、事务和约束,SQLite 的价值在零依赖和单文件。选错场景,跑分再高也没用;选对场景,SQLite 的 766ms 也足够。

需要提醒的是,上面的数字全部来自单机、单进程、缓存状态未加控制的场景。换机器、改 shared_buffers、调整数据分布,绝对值都会变;能稳定复现的是数量级关系——列存向量化在聚合查询上比行存快一个数量级以上。

数据来源与检索时间

检索时间 2026-11-10。三组耗时、导入时间、文件体积均为本机实测。

来源URL对应数据
DuckDB 官方《Why DuckDB》https://duckdb.org/why_duckdb列式向量化执行引擎;"PostgreSQL、MySQL、SQLite 逐行顺序处理";进程内运行带来的数据传输优势;OLAP 负载定义
SQLite 官方《Appropriate Uses For SQLite》https://www.sqlite.org/whentouse.html单写者限制("只允许一个写者");数据分析场景的适用性;"SQLite competes with fopen()" 的定位;低于 10 万次/天的网站建议
PostgreSQL 18 文档《Parallel Query》https://www.postgresql.org/docs/18/parallel-query.htmlmax_parallel_workers_per_gather 与 max_parallel_workers 的关系(实际 worker 数受多重限制)

核验说明

  • 数据生成、导入、查询与计时脚本均为本机执行;每条查询 3 次取中位数,PostgreSQL 用 \timing 输出的 Time:,DuckDB/SQLite 用 Python 计时。
  • PostgreSQL 的并行度对照(默认 / 8)与 DuckDB 的线程数对照(默认 / 1)都已实测,结果在第 3 节。
  • 未控制的变量:三个引擎的缓存状态没有统一处理(首次查询前未强制清缓存),因此这里比较的是"常见使用形态下的相对差异",不是严格微基准;数据分布是均匀随机,真实业务数据的选择性会改变绝对值。
  • 没有测:多客户端并发、写入吞吐、超过内存的数据量(>32GB)。这三项都会显著改变结论,尤其并发写——那是 DuckDB 的弱项。