ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

DuckDB与MySQL查询性能对比实测与分析

DuckDB与MySQL查询性能对比实测与分析

1. 为什么需要对比DuckDB与MySQL的查询性能

在数据爆炸式增长的时代,我们经常需要处理GB甚至TB级别的数据集。作为开发者或数据分析师,选择正确的数据库引擎对工作效率有着决定性影响。最近我在处理一个包含3亿条记录的电商交易数据集时,就面临这样的选择:是继续使用传统的MySQL,还是尝试新兴的DuckDB?

DuckDB作为一个嵌入式分析型数据库,近年来在数据科学社区获得了广泛关注。它号称能在单机上高效处理分析型查询,特别适合OLAP场景。而MySQL作为最流行的关系型数据库之一,在OLTP场景中表现优异。但它们在超大数据集下的表现究竟如何?这正是我决定进行实测的原因。

2. 测试环境与数据集准备

2.1 硬件与软件配置

为了确保测试结果的可比性,我使用了以下一致的测试环境:

  • 硬件配置:

    • CPU: AMD Ryzen 9 5900X (12核24线程)
    • 内存: 64GB DDR4
    • 存储: 1TB NVMe SSD (读写速度3500MB/s)
    • 操作系统: Ubuntu 22.04 LTS
  • 软件版本:

    • DuckDB: v0.9.2
    • MySQL: 8.0.34
    • Python: 3.10.12 (用于编写测试脚本)

2.2 测试数据集

我生成了一个模拟电商交易数据集,包含以下主要表结构:

-- 用户表 CREATE TABLE users ( user_id BIGINT PRIMARY KEY, username VARCHAR(50), registration_date DATE, tier VARCHAR(20) ); -- 商品表 CREATE TABLE products ( product_id BIGINT PRIMARY KEY, name VARCHAR(100), category VARCHAR(50), price DECIMAL(10,2) ); -- 交易表(主测试表) CREATE TABLE transactions ( transaction_id BIGINT PRIMARY KEY, user_id BIGINT, product_id BIGINT, quantity INT, transaction_time TIMESTAMP, payment_method VARCHAR(20), FOREIGN KEY (user_id) REFERENCES users(user_id), FOREIGN KEY (product_id) REFERENCES products(product_id) );

数据集规模:

  • 用户表:1000万条记录
  • 商品表:100万条记录
  • 交易表:3亿条记录(约120GB)

提示:生成测试数据时,我使用了Python的Faker库确保数据分布的合理性,这对后续查询性能测试至关重要。

3. 数据库配置优化

3.1 MySQL配置调优

为了充分发挥MySQL在大型数据集下的性能,我进行了以下关键配置调整:

# my.cnf 关键配置 [mysqld] innodb_buffer_pool_size = 32G innodb_buffer_pool_instances = 12 innodb_io_capacity = 2000 innodb_io_capacity_max = 4000 innodb_flush_neighbors = 0 innodb_read_io_threads = 16 innodb_write_io_threads = 16 innodb_purge_threads = 4 innodb_change_buffering = all innodb_stats_persistent = ON innodb_stats_auto_recalc = ON

这些配置主要针对:

  • 增大缓冲池以缓存更多数据
  • 优化I/O线程数匹配现代NVMe SSD的性能
  • 禁用相邻页刷新(对SSD无益)
  • 确保统计信息准确

3.2 DuckDB的配置

DuckDB作为嵌入式数据库,配置相对简单,但仍有几个关键参数:

import duckdb # 创建连接时配置 conn = duckdb.connect(':memory:') conn.execute("PRAGMA threads=24") # 使用所有CPU线程 conn.execute("PRAGMA memory_limit='32GB'") # 限制内存使用

DuckDB的自动优化能力较强,主要需要设置:

  • 并行线程数
  • 内存限制
  • 对于持久化存储,可以配置WAL等参数

4. 基准测试设计与执行

4.1 测试查询设计

我设计了5类典型查询来评估性能:

  1. 简单聚合查询

    SELECT COUNT(*), SUM(quantity), AVG(quantity) FROM transactions WHERE transaction_time BETWEEN '2023-01-01' AND '2023-12-31';
  2. 复杂连接查询

    SELECT u.tier, p.category, COUNT(*) as transaction_count FROM transactions t JOIN users u ON t.user_id = u.user_id JOIN products p ON t.product_id = p.product_id WHERE t.transaction_time BETWEEN '2023-01-01' AND '2023-12-31' GROUP BY u.tier, p.category ORDER BY transaction_count DESC LIMIT 100;
  3. 窗口函数查询

    SELECT user_id, transaction_time, SUM(quantity) OVER (PARTITION BY user_id ORDER BY transaction_time RANGE BETWEEN INTERVAL 7 DAY PRECEDING AND CURRENT ROW) as weekly_quantity FROM transactions WHERE transaction_time BETWEEN '2023-06-01' AND '2023-06-30';
  4. 子查询与CTE

    WITH high_value_users AS ( SELECT user_id FROM transactions GROUP BY user_id HAVING SUM(quantity * (SELECT price FROM products p WHERE p.product_id = t.product_id)) > 10000 ) SELECT u.username, COUNT(*) as transaction_count FROM transactions t JOIN users u ON t.user_id = u.user_id WHERE t.user_id IN (SELECT user_id FROM high_value_users) GROUP BY u.username;
  5. 全表扫描

    SELECT * FROM transactions WHERE quantity > 5 ORDER BY transaction_time DESC LIMIT 100000;

4.2 测试方法

每个查询执行5次,取中间3次的平均时间作为结果。测试前:

  • 重启数据库服务
  • 清空系统缓存(echo 3 > /proc/sys/vm/drop_caches)
  • 预热数据库(先执行一次查询)

测试脚本记录:

  • 查询执行时间
  • 内存使用情况
  • CPU利用率
  • I/O吞吐量

5. 测试结果与分析

5.1 查询性能对比

查询类型MySQL (秒)DuckDB (秒)差异
简单聚合查询12.343.21-74%
复杂连接查询45.678.92-80%
窗口函数查询78.2315.45-80%
子查询与CTE210.4532.67-84%
全表扫描56.7842.12-26%

5.2 资源使用情况

指标MySQLDuckDB
峰值内存使用28GB18GB
平均CPU利用率65%95%
磁盘读取量120GB90GB

5.3 关键发现

  1. 分析型查询优势:DuckDB在涉及聚合、连接和窗口函数的查询中表现尤为出色,性能提升达80%以上。这得益于其列式存储和向量化执行引擎。

  2. 全表扫描差距较小:当需要进行全表扫描时,两者的差距缩小到26%,说明I/O瓶颈成为主要限制因素。

  3. 并行化效率:DuckDB能更好地利用多核CPU,在复杂查询中将CPU利用率推到95%,而MySQL仅达到65%。

  4. 内存管理:DuckDB的内存使用更为高效,相同查询下比MySQL少用约35%的内存。

6. 深入原理:为什么DuckDB更快?

6.1 存储引擎差异

MySQL使用传统的行式存储(InnoDB),适合OLTP场景:

  • 按行存储数据
  • 优化单行读写
  • B+树索引结构

DuckDB使用列式存储,适合OLAP场景:

  • 每列单独存储
  • 压缩效率高
  • 向量化处理

6.2 执行模型对比

MySQL的火山模型:

  • 一次处理一行
  • 大量虚函数调用
  • 难以向量化

DuckDB的向量化模型:

  • 一次处理一批数据(1024行)
  • 减少函数调用开销
  • 更好的CPU缓存利用率

6.3 优化器差异

MySQL的优化器:

  • 为OLTP优化
  • 侧重索引使用
  • 连接顺序优化有限

DuckDB的优化器:

  • 为分析查询优化
  • 高级统计信息
  • 更智能的连接顺序选择
  • 自适应执行计划

7. 实战建议与注意事项

7.1 何时选择DuckDB

  • 数据分析工作流:作为Pandas的替代,处理GB级数据
  • 嵌入式分析应用:需要轻量级但强大的分析能力
  • ETL管道中间处理:复杂转换和聚合
  • 数据科学实验:快速迭代和探索

7.2 何时坚持使用MySQL

  • 高并发OLTP:大量短事务
  • 已有成熟架构:与现有系统集成
  • 需要完整ACID:复杂的事务需求
  • 已有专业DBA团队:熟悉MySQL调优

7.3 性能优化技巧

对于DuckDB:

  • 合理设置memory_limit防止OOM
  • 使用PRAGMA threads设置合适的并行度
  • 对常用查询创建物化视图
  • 考虑分区大表

对于MySQL:

  • 为分析查询创建适当的列索引
  • 考虑使用查询缓存
  • 对大表进行分区
  • 定期分析表更新统计信息

7.4 常见问题解决

DuckDB内存不足错误

# 增加内存限制 conn.execute("PRAGMA memory_limit='64GB'") # 或使用更激进的溢出到磁盘策略 conn.execute("PRAGMA temp_directory='/path/to/large/disk'")

MySQL连接性能差

-- 确保连接字段有索引 CREATE INDEX idx_transactions_user_id ON transactions(user_id); CREATE INDEX idx_transactions_product_id ON transactions(product_id); -- 调整join_buffer_size SET GLOBAL join_buffer_size = 256*1024*1024;

8. 扩展思考:混合架构的可能性

在实际生产环境中,我们不必非此即彼。可以考虑以下混合架构:

  1. MySQL作为主OLTP数据库:处理日常事务
  2. DuckDB作为分析侧车:定期从MySQL导出数据到DuckDB文件
  3. 使用DuckDB的Postgres扫描器:直接查询MySQL数据而不复制

这种架构结合了两者的优势:

  • MySQL保证事务完整性和高并发
  • DuckDB提供快速分析能力
  • 通过合理的数据同步策略保持一致性

我在一个电商分析系统中实现了这种架构,将核心报表的生成时间从小时级降到分钟级,同时不影响线上交易系统性能。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进