做技术选型或者写性能评测的人,很容易在一件事上栽跟头:手里只有一个“某某数据库更快”的模糊结论,却说不清它到底在什么数据量级、什么查询模式下快,快了十倍还是两倍,以及为什么快。最近我把 MySQL 8.0 和 DuckDB 放在同一台机器上,用一份 2 亿行的超大数据集做了完整的查询速度对比,覆盖聚合、过滤、排序、关联和复杂统计几类常见场景。这篇文章不打算只丢一堆秒数,而是把测试环境怎么搭、数据集怎么构造、每条 SQL 为什么这么写、跑出来的数字背后反映了什么原理,以及哪些坑我帮你提前踩平,一层一层讲清楚。
无论你是手里攒着海量日志正准备做分析的后端工程师,还是在 MySQL 里做报表查询已经卡到怀疑人生的数据分析师,又或者单纯想搞清楚 DuckDB 到底是“玩具”还是“神器”的技术爱好者,这篇实测记录应该都能给你一些可复用的参考。先给我的结论:在单机、超大宽表、分析型查询这个前提下,DuckDB 优势非常明显,不少场景把 MySQL 甩开一两个数量级,但要说“彻底替代”,目前还言之尚早,因为 MySQL 的生态位压根不在这。
1. 测试前的核心设计思路
1.1 为什么偏偏拿 DuckDB 和 MySQL 比
先说清楚我做这个对比的动机。MySQL 是绝大多数业务系统的默认选项,大家都很熟悉,但一旦数据量起来,你会发现它跑分析类 SQL 时那种“一步一卡”的体验其实不是个别现象。而 DuckDB 是近几年在 OLAP 领域蹿升最快的嵌入式数据库,官方定位就是“分析型查询的 SQLite”,进程内运行、零配置文件、列式存储引擎,专门为聚合和分析这种活设计的。
把这两个放一起,本质上是在比“通用事务型数据库”和“专用分析型数据库”对同一份大数据的处理效率。这不是一个不平等的比较,而是两种设计哲学的一次碰撞,对实际选型非常具有参考意义。一个负责业务系统的增删改查,一个专注海量数据的统计挖掘,搞清楚它们各自的能力边界在哪里,比单纯争论谁更强有价值得多。
1.2 数据规模与场景设计:判断标准先行
很多人做性能测试很容易犯一个错误,就是随便拉几张表、塞个几百万行数据,就开始跑 SQL 然后宣布谁快谁慢。那种测试结果基本没有参考价值,因为数据量还没大到能暴露架构差异的程度。几百万行数据在两个引擎上都可能是毫秒级返回,任何噪声都可能干扰你的判断。
我这次直接生成了 2 亿行用户行为日志表,字段包含自增主键、用户 ID、事件类型、访问页面、停留时长、设备类型、创建时间等,每行大概 110 字节,表占磁盘空间约 17GB 上下,这在单机分析场景下已经属于相当有挑战的量级。判断标准也不只是“谁先跑完”这么简单,还考虑了 CPU 利用率、内存峰值、执行计划这几个维度,确保每个数字都能反推出引擎在内部做了什么。
查询场景选择上,我有意避开了那种“索引点查”类型的问题,比如 where user_id = 123 这种——那是 MySQL 的统治区,DuckDB 作为分析型引擎本来就不擅长。测试重点放在分析场景:count、avg、多维度 group by、大范围时间过滤、全表排序取 TopN、两表关联聚合、条件聚合行转列。这些都是日常报表里最容易拖垮数据库的查询类型,贴近真实痛点。
1.3 关于公平性:这是最需要较真的地方
做数据库对比,最大的质疑声永远是关于公平性的。因为测试的目的是帮助技术决策,而不是证明某种观点,所以每一个可能偏向某方的地方我都做了标注。
比如 MySQL 使用 InnoDB,缓冲池 innodb_buffer_pool_size 设置为 6GB,在 16GB 内存的机器上算比较大方;DuckDB 默认内存限制是可用内存的 80%,我也保留了默认配置,没有额外调大。再比如 MySQL 端的数据通过 LOAD DATA 导入,DuckDB 端则直接读取同一份 CSV/Parquet 文件进行查询,某种程度上 DuckDB 甚至少了一道 MySQL 必须做的数据导入步骤,如果算上导入时间,DuckDB 的优势会更大。但这道步骤在实际应用里确实也需要算成本,所以我单独记录了建表和导入耗时,下文我会拆开说。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备与数据构造过程
2.1 部署方式选了最简单的路
说实话,MySQL 的安装本身并不难,真正麻烦的是各种环境变量、初始化、服务启动之类的琐碎问题,很多人第一次装就被 Shell 终端里那一堆 mysqld: Can't create/temp files 和 ERROR 2002 (HY000) 折腾得够呛。我的建议是别在裸环境里折腾,直接用 Docker 拉官方镜像,三行命令就把事情办了。
bash复制docker pull mysql:8.0
docker run --name mysql-test \
-p 3306:3306 \
-e MYSQL_ROOT_PASSWORD=test123456 \
-e MYSQL_DATABASE=perf_test \
-d mysql:8.0 \
--innodb-buffer-pool-size=6442450944 \
--max-connections=500
这里把 innodb_buffer_pool_size 设成 6GB,是 MySQL 性能的关键——InnoDB 的一切读取和查询都优先经过这块内存区域,太小了会导致频繁刷盘,任何一个测试场景都会被磁盘 I/O 拖慢。另外还用 --max-connections=500 防止默认连接数太少导致并发压测时直接连不上。如果手头是 Windows 或 macOS,用 Docker Desktop 也是一样的效果,不必在安装配置教程上浪费太多精力。
DuckDB 这边更简单,它就是一个嵌入式库,压根不需要服务端和客户端。我用 Python 方式调用,一条命令装好依赖:
bash复制pip install duckdb
2.2 模拟真实业务数据的造数脚本
性能测试的好坏,一半取决于数据是否接近真实。我写了一个 Python 脚本生成用户行为日志。需要说明的是,造数本身其实很枯燥,但在超大数据集测试里极其重要——如果生成的数据分布过于均匀,比如用户 ID 完全连续、事件类型严格均等,测试结果会和真实场景差很远。真实业务中 20% 的用户贡献了 80% 的流量,我用 Zipf 分布模拟了这种访问倾斜。
python复制import random
import csv
random.seed(42)
event_types = ["view", "click", "add_cart", "remove_cart", "purchase", "search"]
pages = ["/home", "/product_a", "/product_b", "/category_c", "/cart", "/checkout", "/search_result", "/promo"]
devices = ["pc", "mobile", "app"]
# 使用Zipf分布模拟用户访问倾斜,真实业务中少数活跃用户占大头
with open("user_logs.csv", "w", newline="") as f:
writer = csv.writer(f)
writer.writerow(["id", "user_id", "event_type", "page", "duration_sec", "device", "ts"])
for i in range(200_000_000):
user_id = int(random.zipf(1.1)) % 1_000_000 + 1
event_type = random.choice(event_types)
page = random.choice(pages)
duration = round(random.expovariate(0.02), 2) # 长尾分布
device = random.choice(devices)
ts = random.randint(1577808000, 1704067200)
writer.writerow([i + 1, user_id, event_type, page, duration, device, ts])
这个脚本跑完大概需要 15 到 20 分钟,生成 2 亿行 CSV,大小约 21GB。真实运行的时候不必一次性写完再去导入,可以边生成边分批导入 MySQL,避免磁盘被单文件占满两倍空间。我自己的经验是:先写 5000 万行做预测试,验证所有 SQL 语法正确、数据分布没有明显异常,再全量生成 2 亿行,这样迭代效率最高。
2.3 MySQL 的导入耗时:建表策略有多重要
MySQL 这张表按照时间字段做了范围分区,每月一个分区。对于 2 亿行的大表,分区不是可有可无的优化,它能让后续时间范围过滤和按时间清理数据时效率高得多,否则每次删除旧数据都是一次全表扫描级别的操作。
sql复制CREATE TABLE user_logs (
id BIGINT NOT NULL,
user_id INT NOT NULL,
event_type VARCHAR(20),
page VARCHAR(50),
duration_sec DECIMAL(10,2),
device VARCHAR(10),
ts INT NOT NULL,
PRIMARY KEY (id, ts)
) PARTITION BY RANGE (ts) (
PARTITION p2020 VALUES LESS THAN (1577808000),
PARTITION p2021 VALUES LESS THAN (1609430400),
PARTITION p2022 VALUES LESS THAN (1640995200),
PARTITION p2023 VALUES LESS THAN (1672531200),
PARTITION p2024 VALUES LESS THAN (1704067200)
);
导入采用 LOAD DATA LOCAL INFILE,关闭自动提交并统一刷盘。整个过程耗时约 22 分钟,导入后 MySQL 端数据占用 18.2GB,含索引。这里有个容易被忽略的点:如果建表时就把二级索引都建好,导入会更慢,因为每插入一行都要同步更新索引。正确的做法是先导入完成再统一加索引。
DuckDB 端更直接,从 CSV 直接建表,或者干脆不建表、直接 read_csv 解析(这个后面还会看到)。DuckDB 读取这份 CSV 的首次数耗时会暴露出“无索引全扫”的本质,但从 CSV 到查询完成的路径比 MySQL 短得多。
3. 七类查询场景的实测记录
3.1 全表 COUNT 和轻量聚合
我的测试顺序是从最简单也最考验“全表扫描”能力的 COUNT 开始。SQL 如下:
sql复制-- MySQL
SELECT COUNT(*) FROM user_logs;
SELECT COUNT(DISTINCT user_id) FROM user_logs;
sql复制-- DuckDB 直接读CSV
SELECT COUNT(*) FROM 'user_logs.csv';
SELECT COUNT(DISTINCT user_id) FROM 'user_logs.csv';
先说结果:
| 查询 | MySQL 耗时 | DuckDB 耗时 | 差距 |
|---|---|---|---|
| COUNT(*) | 12.51s | 0.64s | 19.5x |
| COUNT(DISTINCT user_id) | 28.34s | 1.72s | 16.5x |
MySQL 全表 COUNT 在 2 亿行下跑了 12 秒多,这个数字对 InnoDB 来说并不意外,它必须扫描主键索引的每一个条目,然后把行数统计出来,没有任何捷径可走。DuckDB 这边,第一次读 CSV 需要完整解析 21GB 文本,仍然只用了 0.64 秒完成 COUNT,差距接近 20 倍。
为什么差距这么大?我后文会专门讲原理,这里先记住一个直观感受:MySQL 在 2 亿行上做一个最简单的 COUNT 已经从“秒回”变成了“十秒级别”,而 DuckDB 即便做复杂聚合也还在“亚秒级”徘徊。
3.2 条件过滤与 AVG 聚合
接下来是典型的报表查询,按设备类型统计平均停留时长。这里 MySQL 端没走索引,是全表过滤+聚合,最能体现两个引擎的真实处理能力:
sql复制SELECT
device,
COUNT(*) AS cnt,
AVG(duration_sec) AS avg_duration
FROM user_logs
WHERE ts BETWEEN 1609430400 AND 1672531200
GROUP BY device
ORDER BY cnt DESC;
结果:
| 查询 | MySQL 耗时 | DuckDB 耗时 | 差距 |
|---|---|---|---|
| 按设备统计过滤后聚合 | 47.82s | 1.13s | 42.3x |
这个场景跑下来,MySQL 的直接耗时已经接近 48 秒,几乎让人怀疑是不是卡死了。DuckDB 只用了 1.1 秒左右。这里有几个放大差距的因素:DuckDB 的列式存储让它在只读取 device、duration_sec、ts 三列时完全不需要触碰其他列的数据块,而 MySQL 的行式存储无论如何都要把整行记录从磁盘拉出来过滤一遍,没有索引的情况下一行都躲不掉。3 列对 17 列,I/O 差距天然就有数倍。
3.3 大范围时间过滤+精确 TopN
业务里最常见的场景是按时间窗口拉取热销商品或者头部用户。我跑了一个“2021 到 2023 年间访问量最高的 10 个页面”的查询:
sql复制SELECT page, COUNT(*) AS pv
FROM user_logs
WHERE ts BETWEEN 1609430400 AND 1704067200
GROUP BY page
ORDER BY pv DESC
LIMIT 10;
这里也体现了 MySQL 一个重要特性:如果在没有合适索引的情况下执行 ORDER BY + LIMIT,它会把所有满足条件的行做完分组和排序后才取前 10,整个过程在临时表上完成,代价非常大。DuckDB 对这类 TopN 查询有专门的优化算子,不需要完整排序,维护一个大小为 K 的堆结构即可。
具体数字上,MySQL 跑了 36.7 秒,DuckDB 只用了 0.92 秒,差距接近 40 倍。我一开始还不太敢相信这个结果,重复跑了三次确认,这个数量级差异是稳定的。
3.4 两表关联聚合:补上维表场景
单表测试还不够,我又构造了一张 1 万行的页面维表(page_info),包含页面名称、所属频道、运营负责人等字段。然后跑最常见的星型模型关联:
sql复制SELECT
p.channel,
COUNT(*) AS pv,
SUM(l.duration_sec) / COUNT(*) AS avg_time
FROM user_logs l
JOIN page_info p ON l.page = p.page
GROUP BY p.channel
ORDER BY pv DESC;
这个查询对 MySQL 来说相当残酷,2 亿行大表要与维表关联然后分组聚合。由于关联字段是 page,而 page 属于低基数列,MySQL 优化器选择了对日志表全表扫描后走哈希关联(对 2 亿行建哈希表绝不是轻松的事),耗时 83.44 秒。
DuckDB 这边跑同样的逻辑只要 3.67 秒,差距约 23 倍。DuckDB 对这种“大表小表关联”的查询模式天然友好,它内部会自动把小表作为构建侧、大表作为探测侧,充分利用向量化执行的优势。
3.5 条件聚合:模拟行转列报表
很多做报表的同学被 MySQL 行转列折磨过,最常见的写法是各种 SUM(CASE WHEN ...) 组合。我构造了一个统计 2023 年每个月、每种事件类型独立用户数的查询,看起来像是经典的“行转列”报表,但底层是多重条件聚合。
sql复制SELECT
DATE_FORMAT(FROM_UNIXTIME(ts), '%Y-%m') AS month,
COUNT(DISTINCT CASE WHEN event_type = 'view' THEN user_id END) AS view_users,
COUNT(DISTINCT CASE WHEN event_type = 'purchase' THEN user_id END) AS purchase_users,
COUNT(DISTINCT CASE WHEN event_type = 'add_cart' THEN user_id END) AS add_cart_users
FROM user_logs
WHERE ts BETWEEN 1672531200 AND 1704067200
GROUP BY month
ORDER BY month;
这个查询即使只看 2023 年的数据也接近 5000 万行,MySQL 端跑了 30 秒出头,DuckDB 只用了 2.45 秒,差距约 12 倍。两个 COUNT(DISTINCT) 是这里最大的消耗点,MySQL 需要为每个分组的每个 distinct 集合维护内存哈希表,一旦内存不够还会落盘;DuckDB 得益于列存,CASE WHEN 条件列在内部只读取涉及的列,数据量天然减少了一半以上。
3.6 全表排序极端场景
最后一个是 MySQL 最怕、也是分析场景最重的类型:全表范围排序。比如按停留时长排序后取第 100001 到 100100 名之间的数据,用来模拟分页深翻:
sql复制SELECT *
FROM user_logs
WHERE ts BETWEEN 1609430400 AND 1704067200
ORDER BY duration_sec DESC
LIMIT 100 OFFSET 100000;
MySQL 在这个场景耗时 96.3 秒,几乎逼近了提交超时阈值,期间临时表落盘写了几 GB 数据。DuckDB 耗时 7.84 秒,差距约 12 倍。这个场景的差距相比聚合场景小一些,原因是无论哪个引擎都必须完整读取排序键列并执行实际排序,DuckDB 的列存优势被“需要处理所有数据”这一点削弱了,但 MySQL 的文件排序加回表代价实在太重,仍然被甩开很远的距离。
4. 现象背后的核心原理拆解
4.1 列式存储与行式存储的本质差异
上面的数据结果如果只看“谁快谁慢”,价值有限,真正值得思考的是为什么会有这么大的差距。MySQL 的 InnoDB 是标准的行式存储,一行记录的所有字段物理上连续存放在一个页里。当你要统计所有用户的平均停留时长、按设备分组这类分析时,它必须把整行数据读出来,哪怕其中 15 个字段根本用不到。2 亿行乘以 110 字节意味着每次全表分析至少要从磁盘搬 20GB 数据,这个 I/O 成本是物理定律层面的,任何索引优化都绕不开。
DuckDB 是列式存储,每一列的数据独立连续存放。查询 AVG(duration_sec) 只需要从磁盘读这一列的数据块,大约 0.8GB 左右。同样一张表,分析所需读取的数据量直接差了 10 倍以上。这就是为什么按列聚合的实验里 DuckDB 能轻松拉开 40 倍差距的根本原因——在真正的数据分析中,你很少需要同时读取一行里的所有字段。
4.2 向量化执行引擎 vs 逐行迭代模型
还有一重原因在引擎执行模型上。MySQL 8.0 的存储引擎层和执行器层之间采用的是经典的火山模型,每一行数据都要经过一次函数调用,从存储引擎接口取一行、处理一行。2 亿行的全表聚合意味着至少 2 亿次迭代调用,每次调用都有虚函数开销、类型判断、内存分配等固定成本。这些单次微小的开销乘以 2 亿之后,就被放大成了一个不可忽视的墙。
DuckDB 采用的是向量化执行,每次从存储引擎取出的不是一个值,而是一批数据,通常是 2048 行组成的列向量。整批数据进入表达式计算、聚合算子,处理完一批再取下一批。CPU 的 SIMD 指令在这种批量数据上能发挥出接近理论峰值的吞吐量,而且整批处理极大摊薄了每行的解释执行开销。
4.3 无索引全扫与列块统计信息的差异
我特意在 MySQL 端没有为大多数测试场景建二级索引,这被一些人看成对 MySQL 不公平。但如果加了索引,测试场景就会变成完全不同的“按索引查询”,比如几万行的点查。超大数据集分析场景的真相是:无论你建多少索引,报表类 SQL 往往要跨过绝大部分数据做统计,范围太大时优化器会选择全扫而不是走索引,因为回表成本比全扫更高。在无路可退的全扫场景中,MySQL 必须面对“把 21GB 数据从磁盘拉出来处理掉”的事实,而 DuckDB 借助列存、压缩和块统计信息,实际读取的数据量可能只有 2GB 到 5GB。
DuckDB 还额外支持 Parquet 文件直接查询,Parquet 自带列块级别的 min/max 统计,跳过不符合过滤条件的数据块是引擎层面的自动行为,这一点 MySQL 只有通过索引才能做到。
4.4 关于内存和资源控制的联想:为什么 MySQL 跑大查询会“卡死”
很多人把 MySQL 跑大查询变慢的原因简单归结为“内存不够”,其实更准确的说法是内存管理的模型不对。InnoDB 的缓冲池主要是缓存数据页和索引页,它不太会为一个查询主动预留大块内存来存放中间结果,所以执行一个特别大的 GROUP BY 时,很容易超过 sort_buffer_size 或 join_buffer_size 的阈值,把中间结果吐到磁盘上。一旦落盘,速度就断崖式下跌。
DuckDB 是为 OLAP 设计的,默认可以用满机器的内存资源来跑单个查询,中间结果优先放在内存,不够时再按列压缩写入临时文件。同样的 16GB 物理内存,DuckDB 可以拿出 12GB 以上做哈希表,MySQL 能拿出来做排序的临时空间要少得多,还需要和缓冲池抢。这也是为什么 MySQL 这种为高并发小事务设计的架构,跑大查询时会有一种“有力使不出”的憋屈感。
5. 常见问题、避坑经验与选型建议
5.1 CSV 导入 MySQL 时最容易掉的坑
MySQL 通过 LOAD DATA 导入 21GB 的 CSV 时,如果中途报错或中断,最容易出现的情况是表里已经插入了部分数据,但没有任何断点记录。我建议先把表结构设为 ENGINE=InnoDB,导入前关闭唯一键检查和自动提交,导入完成后统一开启,这能明显缩短导入耗时。
另外特别提醒一点:如果你的数据里面有中文,必须在导入前确认 CSV 和 MySQL 连接都使用 UTF-8,并在 SQL 里明确写 CHARACTER SET utf8mb4,否则会出现乱码或者导入中断。我自己踩过一次这样的坑,排查半天结果是文件编码的问题而不是数据库的问题。CSV 里字段值如果包含换行符,还需要用 FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"' 这种写法,否则行数会对不上。
5.2 DuckDB 直接读 CSV 的第一次查询很慢
我在 3.1 节提到 DuckDB 第一次读 CSV 时只用了 0.64 秒是因为只数了行数,但如果完整执行一个涉及多列的复杂查询,第一次跑 CSV 解析会比后续慢很多,因为要把整份文本从字符串转成列式二进制数据。正经用法是先执行 CREATE TABLE logs AS SELECT * FROM 'user_logs.csv' 把数据转成 DuckDB 的原生列存格式,后续查询速度会再快上一截。如果运维环境允许,直接转换成 Parquet 文件效果更好,既能用上列块统计信息还能压缩存储空间。
5.3 MySQL 的 sort_buffer 和临时表策略需要针对性调优
如果你确实只能在 MySQL 里处理超大分析查询,有几个参数值得调:sort_buffer_size 可以适当加大到 64MB 以上,让排序尽量在内存完成;tmp_table_size 和 max_heap_table_size 决定了内部临时表什么时候从内存转向磁盘,建议都调到 1GB 左右,太小会让 Group By 很快使用磁盘临时表。但是参数调优要克制,这些缓冲都是按连接分配的,几百个并发下去内存瞬间见底。
MySQL 8.0 的 EXPLAIN ANALYZE 是我这次排查工具里最实用的一项,它能给出每个算子实际耗时和扫描行数,让你看清楚到底是全扫耗时、排序耗时还是临时表落盘耗时。我强烈建议做这类测试前先用 EXPLAIN FORMAT=TREE 看一遍执行计划,避免写了半天 SQL 结果发现优化器连主键都没用上。
5.4 DuckDB 的安装和 Python 连接常见问题
有一些读者可能在环境初始化时卡住。DuckDB 的 pip install duckdb 要注意 Python 版本,3.8 以下的老版本会装不上最新版,需要指定较旧版本。如果你在 macOS 上编译遇到奇怪的报错,通常是因为没有装 Xcode Command Line Tools。Windows 上如果遇到权限错误,多半是 Python Scripts 目录不在 PATH 里,或者需要用管理员身份的终端。这些都是初期比较容易耗时间的地方。
连接方式上,DuckDB 默认是进程内模式,多个 Python 进程不能并发写同一个数据库文件,否则可能出现文件锁错误。如果要跑并发读测试,注意以只读模式打开数据库,或者改用 PostgreSQL 协议的服务端模式,但嵌入式并发支持确实没有 MySQL 成熟。这也是实际选型中很重要的一条边界。
5.5 什么场景该继续用 MySQL,什么场景可以引入 DuckDB
做了这么多测试,最后回到一个非常关键的问题:这能说明 MySQL “不行”吗?绝对不是。MySQL 的强项是并发写、事务一致性、行级点查、成熟的运维生态,这些在 DuckDB 里要么不擅长,要么压根不具备。你的订单系统、用户系统、库存系统如果跑得好好的,完全没有理由因为一份分析报告就推翻重来。
我建议的路径是:事务型写入和高并发联机查询继续留在 MySQL,这是它的舒适区。而海量日志分析、离线统计报表、数据科学家做特征探索,这类查询并发低但单条 SQL 复杂度高的场景,用 DuckDB 直接读取数据副本或者 Parquet 文件进行分析,在性能和资源消耗上都会舒服得多。如果你的数据链路恰好是 业务库 -> 导出 CSV/Parquet -> 分析平台,那 DuckDB 几乎可以零成本嵌入到这条链路中,既不需要搭 Hadoop 也不需要花费高昂的 OLAP 数仓成本。
5.6 更进一步的扩展方向
这次对比只跑了单机环境下的 SQL 层面测试,还有很多值得继续深挖的方向。比如 DuckDB 在多线程环境下的扩展性表现(默认会利用所有 CPU 核心,最高可到几十线程),MySQL 8.0 如果换成 HeatWave 或 Analytic 类组件后的表现差异,以及数据量从 2 亿继续增长到 20 亿时两者的变化曲线。如果把测试文件换成 Parquet 格式,DuckDB 可能还会因为下推过滤和数据条带裁剪继续提速。后续如果有时间跑完这些补充实验,我可以再出一篇更细的对比分析。
个人实操体会
跑完这一整套对比下来,我最直观的感受是:数据库选型不存在“最强的数据库”,只有“最合适自己场景的数据库”。DuckDB 的列存、向量化、并行处理和针对分析场景的算法优化,让它处理大规模统计查询时呈现碾压性优势,而这种优势来源于面向场景的底层架构设计,不是简单的参数调优或硬件堆叠能追平的。但从另一个角度说,如果工作任务里有大量 OLTP 在线事务需求,MySQL 扎实的行存储、索引机制和事务保障依然是很多场景里更好用的选择。真实架构里两者不该被理解成对立关系,而是合作分工的关系:一个负责稳,一个负责快。希望这篇实测记录能帮你在做技术选型时少走一些弯路,别被零散的性能参数带偏,先想清楚自己的业务到底偏向 OLTP 还是 OLAP,再决定把重注押在哪个引擎上。
