原文地址:https://bgithub.xyz/DBatUTuebingen-Teaching/didi-ws2526
数据库查询性能的演进:从脚本到并行优化
作者:Torsten Grust
时间:2025/26年冬季学期
单位:德国图宾根大学
引言:性能工程的起点
数据库管理系统(DBMS)通过充分利用现代计算机架构中的CPU多线程、内存层次结构(DRAM、缓存)及SSD等存储设备,实现了高效的数据处理。即便是最微小的性能优化,在面对数百万行数据处理时,也会产生显著的放大效应。
本文以一个简单的查询任务为切入点,逐步展示从脚本语言到手工优化的C代码,再到现代DBMS(DuckDB)在性能上的巨大差异。该任务为:读取TPC-H基准测试中的lineitem表(约720MB),并对第五列quantity字段求和。
一、性能极限:理论下限
在开始优化之前,作者首先测定了硬件的理论性能极限:
- DRAM读取带宽:21 GB/s → 理论查询时间:0.03s
- NVMe SSD:5 GB/s → 0.14s
- USB-C SSD:800 MB/s → 0.90s
- 以太网:2.5 GB/s → 0.28s
这些数值是基于I/O带宽的理想情况,忽略了CPU处理开销,作为后续优化的参考上限。
二、逐步优化:从awk到多线程C
1. awk脚本(解释型语言)
bash
awk -F '|' '{sum += $5} END {print sum}' lineitem.csv
- 查询时间:1.60s
- 吞吐量:471 MB/s
- 问题:CPU成为瓶颈,即使文件缓存在DRAM中也无济于事。
2. Python(字节码解释)
逐行读取、拆分、类型转换。
- 查询时间:2.75s
- 吞吐量:275 MB/s
- 性能更低,适合快速开发但不适合大规模数据处理。
3. C语言 + getline()
使用标准C库逐行解析。
- 查询时间:0.50s
- 吞吐量:1.5 GB/s
- 性能大幅提升,但getline()和atoi()占用大量CPU(58% + 14%)。
4. C + mmap()
使用mmap()一次性将文件映射到内存,避免逐行系统调用。
- 冷缓存:1.6s(受限于磁盘I/O)
- 热缓存:0.42s(吞吐量1.8 GB/s)
- 问题:逐字节扫描查找换行符
\n成为新瓶颈(占53% CPU)。
5. C + mmap + 块级换行搜索
使用64位字块并行查找换行符,利用位运算宏HAS_NL()。
- 查询时间:0.27s
- 吞吐量:2.8 GB/s
- 性能与
strchr()相当,但代码复杂度显著上升。
6. C + mmap + 多线程
将文件按换行符划分为12个分区,每个线程独立求和。
- 查询时间:0.04s
- 吞吐量:18.8 GB/s
- 逼近DRAM带宽上限,展现了多核并行处理的巨大优势。
三、SQL引擎的极致:DuckDB
作者使用DuckDB对同一数据执行SQL查询:
sql
SELECT sum(l_quantity) FROM read_csv('lineitem.csv');
- 查询时间:0.448s(首次读取)
- 将数据导入DuckDB存储格式后:
sql
SELECT sum(l_quantity) FROM lineitem;
- 查询时间:0.002s
- CPU总时间:0.007s
- 吞吐量远超所有手工实现
性能来源分析:
- 列式存储 :只读取
l_quantity列,避免扫描整行。 - 投影下推:查询计划仅访问所需字段。
- 二进制存储:无需重复解析文本。
- 并行执行:自动利用多核。
- 内存驻留:数据完全加载于DRAM。
四、总结:性能工程的启示
| 实现方式 | 查询时间 | 吞吐量 |
|---|---|---|
| awk | 1.60s | 471 MB/s |
| Python | 2.75s | 275 MB/s |
| C + getline | 0.50s | 1.5 GB/s |
| C + mmap | 0.27s | 2.8 GB/s |
| C + mmap + 线程 | 0.04s | 18.8 GB/s |
| DuckDB (SQL) | 0.002s | >20 GB/s |
50年过去了,关系模型的查询优化依然是活跃的研究领域。
即使是个人笔记本电脑,也能实现每秒数GB的数据处理能力。现代DBMS的性能并非依赖"大铁器"或集群,而是通过对硬件特性的深度理解和精细的性能工程实现的。