同一份 200 万行 CSV:DuckDB、PostgreSQL、SQLite 本地分析查询实测
做本地数据分析时,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 |
| DuckDB | 2.0.0(Python 包,进程内) |
| SQLite | 3.45.1(Python 标准库) |
| PostgreSQL | 18.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. 结果
| 引擎 | 导入耗时 | 落盘体积 | q1 | q2 | q3 |
|---|---|---|---|---|---|
| DuckDB 2.0.0 | 0.2 s | 21.5 MB | 6 ms | 9 ms | 27 ms |
| PostgreSQL 18.6 | 1.7 s | 164 MB | 110 ms | 96 ms | 412 ms |
| SQLite 3.45.1 | 3.0 s | 156 MB | 766 ms | 730 ms | 905 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=8 | 70 ms(实际计划里 Workers Planned: 3) |
| DuckDB 多线程(默认) | 6 ms |
DuckDB PRAGMA threads=1 | 14.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.html | max_parallel_workers_per_gather 与 max_parallel_workers 的关系(实际 worker 数受多重限制) |
核验说明
- 数据生成、导入、查询与计时脚本均为本机执行;每条查询 3 次取中位数,PostgreSQL 用
\timing输出的Time:,DuckDB/SQLite 用 Python 计时。 - PostgreSQL 的并行度对照(默认 / 8)与 DuckDB 的线程数对照(默认 / 1)都已实测,结果在第 3 节。
- 未控制的变量:三个引擎的缓存状态没有统一处理(首次查询前未强制清缓存),因此这里比较的是"常见使用形态下的相对差异",不是严格微基准;数据分布是均匀随机,真实业务数据的选择性会改变绝对值。
- 没有测:多客户端并发、写入吞吐、超过内存的数据量(>32GB)。这三项都会显著改变结论,尤其并发写——那是 DuckDB 的弱项。