DuckDB vs MySQL:8000万行数据查询性能实测与架构解析
2026/9/16 1:49:45 网站建设 项目流程

1. 起因:一张8000万行的流水表逼我开始对比 DuckDB 与 MySQL

1.1 一个周五下午的翻车现场

上个月底,运营同事扔给我一张接近 8000 万行的订单明细表,让我按渠道、按天做一次销售汇总。我习惯性打开 MySQL,SQL 写好后敲下回车,然后就是漫长的等待。第一次跑到 90 秒还没出结果,我当时还以为是线上业务在抢资源,又等了半分钟,最终 MySQL 给出结果,但整个临时表膨胀得厉害,连带同库的其他查询都受了影响。

这不是我头一回遇到这种情况。早年数据量只有几百万行的时候,MySQL 做点聚合、报表查询都很舒服,但随着公司业务量增长,单表几千万行甚至上亿行后,原本好用的 InnoDB 开始力不从心。那次周五下午,我用一整个晚上研究替代方案,最后在一个技术群里被人安利了 DuckDB。我先是拿同一张表在本地跑了一下,同样的聚合逻辑,MySQL 跑一分多钟的查询,DuckDB 两秒多就回来了。这个差距让我开始认真做一轮“超大数据集下 DuckDB 与 MySQL 查询速度对比”的实测,才有了这篇文章。

1.2 DuckDB 到底是什么定位

很多人第一次听到 DuckDB 会以为它又是一个“新型数据库”,需要部署服务端、配置连接池、做高可用,像 ClickHouse 那样搞一套集群。其实 DuckDB 是嵌入式分析型数据库,简单说,它不是一个服务,而是一个库,你可以把它嵌入到 Python、R、Java 程序里,也可以直接通过 CLI 使用。

它的核心特性是列式存储、向量化执行引擎、自动并行扫描,能直接查询 Parquet、CSV 甚至本地的 MySQL 业务库。这意味着你不需要像 ClickHouse 那样搭建多个节点,只要一台“稍微大一点的机器”,就能在单机上处理几十 GB 到几百 GB 的分析数据。在我当时考虑的几个工具里,ClickHouse 和 Presto 都需要额外运维,DuckDB 几乎零成本落地,最适合这个场景。

这篇对比文章,我会用 TPC-H 生成的 10GB 基准数据,在同一台服务器上分别部署 MySQL 8.0 和 DuckDB,测试三类典型分析查询的耗时差异,然后从架构层面解释为什么会有这么大的差距,最后给出我的选型建议和生产环境配合方案。如果你也正在被“超大数据集 + MySQL 聚合慢”折磨,这篇文章应该能帮到你。

2. 测试设计与数据准备:要对比就在同一批数据上打

2.1 测试环境与版本选择

做性能对比最忌讳“夹带私货”,比如一边是 SSD 一边是机械盘,或者一边内存 64G 一边 8G。我把两个数据库放在同一台服务器上,确保硬件条件完全一致。

项目配置
CPUIntel Xeon E5-2680 v4(8 核分配)
内存32 GB DDR4
存储NVMe SSD,顺序读约 1.5 GB/s
操作系统Ubuntu 22.04 LTS
MySQL8.0.36,InnoDB 引擎
DuckDB1.1.x,通过 Python 3.10 客户端调用
数据集TPC-H SF=10,约 10 GB 原始文本,lineitem 表约 6000 万行

MySQL 我用了官方 APT 源安装,DuckDB 直接用pip install duckdb,没有做任何特殊参数优化,尽量模拟一个开发/数据分析人员拿到默认数据库时的真实体验。

2.2 用 TPC-H 造一份 10GB 的基准数据

我没有直接用业务流水表,因为那张表数据虽然多,但字段分布不一定典型,而且涉及到公司内部数据,不适合公开分享。TPC-H 是数据库领域公认的决策支持基准测试集,包含订单、客户、商品、供应商等 8 张表,能模拟真实的分析查询场景。

用 dbgen 生成 SF=10 的数据,命令如下:

./dbgen -s 10 -f

生成后你会得到一堆以.tbl结尾的文本文件,其中lineitem.tbl约 7.5 GB,orders.tbl约 1.5 GB,其余小表加起来不到 1 GB。这里有一个需要注意的点:dbgen 生成的表结构是标准的,MySQL 和 DuckDB 都能支持,非常适合做跨数据库对比。

2.3 建表差异:MySQL 要加索引,DuckDB 免索引

在 MySQL 里,我按照 TPC-H 标准建表,主键和外键部分按规范添加索引。TPC-H 的 lineitem 表比较特殊,主键是联合主键(l_orderkey, l_linenumber),另外我会给常用的l_shipdatel_partkey等列建上二级索引,因为后面的测试查询要用。建表语句大致如下:

CREATE TABLE lineitem ( l_orderkey INTEGER NOT NULL, l_partkey INTEGER NOT NULL, l_suppkey INTEGER NOT NULL, l_linenumber INTEGER NOT NULL, l_quantity DECIMAL(15,2) NOT NULL, l_extendedprice DECIMAL(15,2) NOT NULL, l_discount DECIMAL(15,2) NOT NULL, l_tax DECIMAL(15,2) NOT NULL, l_returnflag CHAR(1) NOT NULL, l_linestatus CHAR(1) NOT NULL, l_shipdate DATE NOT NULL, l_commitdate DATE NOT NULL, l_receiptdate DATE NOT NULL, l_shipinstruct CHAR(25) NOT NULL, l_shipmode CHAR(10) NOT NULL, l_comment VARCHAR(44) NOT NULL, PRIMARY KEY (l_orderkey, l_linenumber), KEY idx_shipdate (l_shipdate), KEY idx_partkey (l_partkey) ) ENGINE=InnoDB;

DuckDB 这边就简单很多,它是分析型引擎,默认就是列式存储,不需要像 InnoDB 那样建二级索引。我直接建表或者更干脆,在测试时直接用 SQL 查询原始.tbl文件。DuckDB 支持对外部文件的“零导入查询”,这本身就是它的一大卖点。为了公平对比导入后的查询速度,我同时导了一份进 DuckDB。

2.4 导入耗时的对比:谁也别想“白嫖”

把 10GB 文本数据导入 MySQL,我用的是LOAD DATA INFILE,命令类似:

LOAD DATA INFILE '/tmp/lineitem.tbl' INTO TABLE lineitem FIELDS TERMINATED BY '|';

实测导入 lineitem 表大概花了 7 分多钟,如果算上建索引的时间,整体在 10 分钟左右。DuckDB 的导入可以用COPY语句:

COPY lineitem FROM '/tmp/lineitem.tbl' (DELIMITER '|');

同样的数据,DuckDB 的 COPY 导入大约花了 50 秒。这里有人可能会说“这不公平,MySQL 要维护索引”,但这也是现实中的使用成本——MySQL 要支撑 OLTP 业务,必然要建索引,导入慢是它整个设计路线的一部分。

3. 实测跑分:三组典型分析查询的真实耗时

3.1 宽表聚合:GROUP BY 与多列统计

第一组测试我选了 TPC-H 里最经典的 Q1 查询,它在 lineitem 表上做一次大范围过滤,然后按l_returnflagl_linestatus分组,分别计算总量、金额、折扣和行数。这个查询属于典型的全表扫描 + 分组聚合,最能体现分析引擎的扫描和计算能力。

SELECT l_returnflag, l_linestatus, SUM(l_quantity) AS sum_qty, SUM(l_extendedprice) AS sum_base_price, AVG(l_discount) AS avg_disc, COUNT(*) AS count_order FROM lineitem WHERE l_shipdate <= DATE '1998-09-02' GROUP BY l_returnflag, l_linestatus ORDER BY l_returnflag, l_linestatus;

在同时清空系统缓存的前提下,MySQL 第一次执行耗时 81.6 秒,第二次执行因为 lineitem 表数据大多进入了 InnoDB Buffer Pool,耗时降到 46.3 秒。DuckDB 第一次执行耗时 2.8 秒,第二次执行 1.9 秒。我把两个数据库分别冷、热状态下的耗时都列在下面。

场景MySQLDuckDB
冷缓存(首次)81.6 秒2.8 秒
热缓存(二次)46.3 秒1.9 秒

这个差距已经不能用“一点点”来形容了。DuckDB 几乎快了 20 到 40 倍。更大的问题是,MySQL 执行期间占用了大量 CPU 和临时表空间,而 DuckDB 跑同样查询时,资源占用还更平稳。

3.2 范围过滤 + 排序取 TopN:有索引也未必救得回来

第二组测试是业务里最常见的“按时间范围查订单,并按金额排序取前 100 条”。我用的 orders 表约 1500 万行,查询某三个月内的订单,按照订单总金额倒序取前 100。

SELECT o_orderkey, o_orderdate, o_totalprice FROM orders WHERE o_orderdate >= DATE '1995-01-01' AND o_orderdate < DATE '1995-04-01' ORDER BY o_totalprice DESC LIMIT 100;

MySQL 这边,我在o_orderdate上有索引,理论上可以走 range scan,但问题是符合条件的行数太多,MySQL 优化器最后选择了全表扫描 + filesort。实测耗时 14.7 秒(热缓存),DuckDB 耗时 0.9 秒。如果数据量再大一些,MySQL 的 filesort 会把临时结果放到磁盘,耗时会更恐怖。

这个案例让我意识到:MySQL 的二级索引只对“能显著缩小结果集”的查询有效。当一个范围条件命中超过表数据的 10% 甚至 20% 时,优化器宁愿全表扫,因为回表随机 I/O 太贵了。而 DuckDB 根本不依赖索引,它靠的是大批量顺序扫描列数据,这个设计差别直接决定了性能走势。

3.3 多表 JOIN 后聚合:三方关联一场大戏

第三组测试是 TPC-H 里比较重的一个变体:把 customer、orders、lineitem 三张表 JOIN 起来,按客户市场分段和月份统计营收。这个查询涉及 150 万客户行、1500 万订单行和 6000 万明细行。

SELECT c_mktsegment, strftime(o_orderdate, '%Y-%m') AS yyyymm, SUM(l_extendedprice * (1 - l_discount)) AS revenue FROM customer, orders, lineitem WHERE c_custkey = o_custkey AND l_orderkey = o_orderkey AND l_shipdate >= DATE '1996-01-01' AND l_shipdate < DATE '1996-04-01' GROUP BY c_mktsegment, yyyymm ORDER BY yyyymm, c_mktsegment;

MySQL 8.0 已经支持 hash join,但受限于执行器和线程模型,这个查询实测跑了 312 秒,将近 5 分钟。DuckDB 同样逻辑跑了 6.8 秒。中间我还特意用EXPLAIN ANALYZE看过 MySQL 的执行计划,它确实用了 hash join,但临时表和物化结果消耗了大量时间,而且单查询没有真正的多线程并行能力。

这三组测试综合下来,在“超大数据集 + 分析类查询”这个靶向上,MySQL 和 DuckDB 完全不是一个量级。我后面会专门讲为什么,先继续聊聊测试方法里一个容易翻车的地方。

3.4 冷缓存与热缓存的区分:别把不公平测试当结论

网上很多对比测试只跑一次,然后就把数字放出来,这是不严谨的。MySQL 有个 InnoDB Buffer Pool,默认会缓存数据页,第一次查询慢、第二次查询明显加快;DuckDB 虽然有自己的内存池,但数据文件也会被操作系统的 page cache 缓存。如果测试时不区分冷热,很容易得到“被冤枉”的结论。

我这里“冷缓存”的操作方法是:MySQL 重启前先记录innodb_buffer_pool_size,然后执行SET GLOBAL innodb_buffer_pool_dump_at_shutdown=OFF,重启实例确保 Buffer Pool 是空的;同时用sync; echo 3 > /proc/sys/vm/drop_caches清空操作系统文件缓存。DuckDB 侧通常不需要重启,清空 OS page cache 就够了,因为 DuckDB 不太依赖自己的持久化缓存,主要靠 OS 缓存。

为什么要强调这一步?因为如果你的业务是“同样的查询反复跑”,热缓存就是真实体验;但如果你是做 BI 看板、临时取数,每次查询的数据范围可能都不同,冷缓存才是常态。两者都测了,才能对数据库能力有客观认识。

4. 为什么 DuckDB 会快这么多:架构差异不是玄学

4.1 列式存储:只要一列数据就别把整行都翻出来

MySQL 的 InnoDB 是典型的行式存储,一行的所有列在磁盘上是连续存放的。当你执行SUM(l_quantity)时,InnoDB 需要把符合条件的数据行整行读出来,哪怕里面 15 个字段里你只要 1 个字段,也要把整行从磁盘拉到内存。磁盘 I/O 是性能瓶颈,行式存储在这个场景下等于“为了一颗白菜,把整个菜市场搬回家”。

DuckDB 是列式存储,同一列的数据在磁盘上连续存放。查询只涉及l_quantityl_returnflag,就只读这两列,其他 13 列完全不碰。我测过,在同样的 SSD 上,DuckDB 读取一列时的实际磁盘 I/O 可能是 MySQL 的几十分之一。这个差异在宽表上尤其明显,表越宽,行式存储越吃亏。

4.2 向量化执行与批量计算

如果说列式存储减少了数据读取量,那向量化执行就是让“处理数据”这个环节变快。MySQL 传统执行器是“一行一行处理”的迭代模型,每拿到一行数据就执行表达式计算、聚合更新,这种模型的优点是实现简单,缺点是 CPU 分支预测和函数调用开销巨大。

DuckDB 的向量化引擎一次处理一批数据,通常是一批 1024 行或 2048 行。你可以理解为 MySQL 像一个工人逐个检查档案袋,而 DuckDB 是一条流水线,每次把一箱档案推过去,批量完成校验、计算和归类。批量方式能充分利用 CPU 的 SIMD 指令,让一次计算同时处理多个数据,这是 10 倍以上性能差距的一个重要来源。

4.3 压缩、并行扫描与聚合的协同作战

DuckDB 在列式存储之上还会做轻量级压缩,比如字典压缩、位图压缩、RLE 压缩等。压缩的直接收益是磁盘 I/O 更少,数据从磁盘读到内存后,在内存里解压并进行向量化计算,整体吞吐量反而更高。MySQL 也有表压缩和页压缩,但它的主要场景是 OLTP,压缩和解压的 CPU 开销可能拖慢写路径,所以默认并不会启用。

并行扫描方面,DuckDB 默认会把一张表的数据划分成多个范围,用多个线程同时扫描和预聚合,最后再做合并。我的机器是 8 核,跑聚合查询时 CPU 能跑到 6 到 7 个核的利用率。MySQL 8.0 在单条查询上虽然也有一些并行改进,但对这种大范围聚合查询,实际执行中很难把多核用起来,经常是一个线程在单打独斗。

4.4 那 MySQL 的 B+ 树索引去哪了?为什么不救场

有人说“MySQL 不是有大名鼎鼎的 B+ 树索引吗,为什么还会这么慢?”关键在于索引只擅长快速定位少量数据。WHERE id = 123这种点查询,索引能让 MySQL 在毫秒级返回;可一旦查询变成“扫描 6000 万行并做聚合”,索引基本不起作用,只能全表扫描,而且行式存储的全表扫描效率远低于列式存储的顺序扫描。

更要命的是,如果查询条件命中了二级索引,MySQL 还得根据索引里的主键值回表读取整行数据,这会产生大量随机 I/O。随机 I/O 比顺序 I/O 慢一到两个数量级。所以我看到不少朋友给 MySQL 的每个查询字段都建了索引,结果大查询一样慢,原因就在这里:分析型查询要的是全量扫描和并行计算,不是一条一条精确查找。

5. 该用 DuckDB 还是 MySQL?以及让两者协作的实战方案

5.1 OLTP / OLAP 十字路口:选型判断清单

读到这里,你可能会想:“那我是不是可以直接把 MySQL 换成 DuckDB?”我的答案是:得分场景。DuckDB 在超大数据集的分析查询里确实强,但它并不是 MySQL 的“全能替代品”。

场景MySQLDuckDB
高频小事务写入推荐,InnoDB 为写入做了大量优化不推荐,嵌入式引擎不适合高并发写入
点查询(按主键找一行)推荐,B+ 树索引毫秒级返回可以,但并发能力有限
宽表聚合、报表分析容易慢,尤其大表强烈推荐,列存 + 向量化优势巨大
复杂多表 JOIN慢,临时表膨胀推荐,执行引擎更适合哈希连接
高并发在线服务推荐,连接池和权限体系成熟不推荐,单进程多线程模型压不住高并发
数据导入导出一般,LOAD DATA 可接受很强,可直接查外部文件

如果你做的是 OLTP 业务系统,比如电商订单、用户中心、后台管理,MySQL 依然是最稳妥的选择。但如果你跑的是数据分析、报表、临时取数、ETL 清洗,DuckDB 的高性能和零运维会非常讨喜。

5.2 用 DuckDB 的 mysql 扩展直接查业务库

DuckDB 有一个 mysql 扩展,可以直接把 MySQL 当作外部数据源来查询。这个功能在做“快速临时分析”时特别方便,无需导出文件,无需同步任务。基本用法如下:

INSTALL mysql; LOAD mysql; ATTACH 'host=127.0.0.1 user=root password=your_password database=your_db' AS mysqldb (TYPE mysql); SELECT c_mktsegment, SUM(l_extendedprice * (1 - l_discount)) AS revenue FROM mysqldb.lineitem l JOIN mysqldb.orders o ON l.l_orderkey = o.o_orderkey JOIN mysqldb.customer c ON o.o_custkey = c.c_custkey GROUP BY c_mktsegment;

需要注意边界:DuckDB 的 mysql 扩展会把数据从 MySQL 拉到 DuckDB 再进行计算,如果数据量特别大,例如你要拉全表 6000 万行,那受制于 MySQL 端扫描和网络传输,速度不会像查本地列存文件那么快。所以我的经验是:数据量在百万到千万级别、并且有较强过滤条件时,这个扩展非常好用;如果数据量再大,还是建议走“导出 Parquet + 本地分析”的路线。

5.3 一个可落地的“MySQL 负责生产,DuckDB 负责分析”架构

综合我实际使用的场景,我更推荐一个混合架构:MySQL 继续作为业务主库,保证在线交易稳定;DuckDB 作为分析加速层,专门承接报表和复杂查询。

具体做法是,每天凌晨用脚本把 MySQL 里的超大表导出为 Parquet 文件,放到本地磁盘或对象存储,再让 DuckDB 直接查询这些 Parquet 文件。导出任务可以是一条简单的 SQL 配合命令行工具:

mysql -h127.0.0.1 -uroot -p -N -e \ "SELECT * FROM lineitem WHERE l_shipdate >= '1995-01-01'" \ > lineitem_increment.csv python3 - <<'EOF' import duckdb duckdb.sql(""" COPY (SELECT * FROM read_csv('lineitem_increment.csv')) TO 'lineitem_increment.parquet' (FORMAT 'parquet'); """) EOF

Parquet 文件不仅比 CSV 更省空间,DuckDB 读取列存格式时可以跳过无关列,查询速度会再上一个台阶。这个方案的优点是:MySQL 的负担大大降低,DuckDB 的分析性能能得到充分释放,而且整个链路没有引入复杂的分布式组件,一个定时脚本就能搞定。

6. 实操中的几个高频踩坑点

6.1 内存管理:DuckDB 把机器打满怎么办

DuckDB 默认会使用可用内存的较大比例来做缓存和中间结果。单条分析查询可能几秒跑完,但如果同时跑多个 DuckDB 进程,或者并行任务太多,很容易把机器内存打满,甚至触发 OOM。我的习惯是在 DuckDB 会话里显式设置资源上限:

SET memory_limit = '16GB'; SET threads = 6;

threads要根据机器 CPU 核数来定,不是越大越好。如果任务并发多,适当调低线程数反而能减少调度开销,整体吞吐更稳。另外,DuckDB 1.1 之后对内存溢出的支持也变好了,超内存时会 spill 到磁盘,但 spilling 会明显降低性能,能通过memory_limit规划好就尽量规划好。

6.2 MySQL 这边值得调的参数

既然要和 MySQL 共存,那就把 MySQL 也调一调。针对分析类查询,我主要改了innodb_buffer_pool_size,把它从默认的 128MB 调到 16GB,也就是机器内存的一半。这个参数决定了 InnoDB 能缓存多少数据页,对第二次查询的加速效果非常明显。

另外,如果你经常跑大聚合,可以考虑临时表从内存转磁盘的阈值。MySQL 8.0 的临时表默认使用 TempTable 引擎,如果数据量大,tmp_table_sizemax_heap_table_size的设置会影响是否走磁盘临时表。我一般保持默认,因为磁盘临时表虽然慢,但至少不会 OOM,线上稳定优先。

6.3 测试结论的“保质期”:版本、硬件、数据分布都会翻盘

最后说一个比较容易忽略的点:性能对比没有“一劳永逸”的结论。DuckDB 迭代非常快,我半年前测的一个查询在旧版本上要 3 秒,新版本可能 1 秒就出来了;MySQL 同样在持续优化,8.0 的 hash join、后续版本的并发能力都在变强。硬件也很关键,如果你的机器是普通 HDD,MySQL 和 DuckDB 的差距可能没那么夸张,因为瓶颈都在磁盘咋转;如果是 NVMe SSD 加高主频 CPU,DuckDB 的优势会更明显。

数据分布也会影响结果。比如一个表的某个字段只有 3 个取值,用 DuckDB 的 RLE 压缩会非常受益,MySQL 却没法在行式存储里享受这种红利。反过来,如果你的查询每次都命中唯一索引、只取几行,MySQL 会反超 DuckDB。所以我不建议大家直接照搬我这组数字,而是要掌握测试方法,在你自己的数据、你自己的硬件上复现一遍。

我自己现在的工作习惯是:MySQL 继续管业务流水,DuckDB 管分析报表,两边各司其职。超大数据集下的查询速度对比做完了,我更确信一个道理——“快”不光是引擎的功劳,更是选型和场景匹配的结果。你拿分析查询去为难 OLTP 数据库,它自然会吃力;你拿 OLTP 事务去压 DuckDB,它也不擅长。搞清楚数据库的脾气,把它们放到合适的场景里,比单纯鼓吹某个数据库“天下第一”有用得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询