⌘+k ctrl+k
1.4 (LTS)
搜索快捷键 cmd + k | ctrl + k
调优工作负载

preserve_insertion_order 选项

在导入或导出数据集(从/到 Parquet 或 CSV 格式)时,如果数据集远大于可用内存,可能会出现内存不足(out of memory)错误。

Out of Memory Error: failed to allocate data of size ... (.../... used)

在这种情况下,请考虑将 preserve_insertion_order 配置选项设置为 false

SET preserve_insertion_order = false;

这允许系统对任何不包含 ORDER BY 子句的结果进行重新排序,从而可能降低内存占用。

并行处理(多核处理)

行组(Row Groups)对并行处理的影响

DuckDB 基于行组(即存储层面上存放在一起的行集合)来并行化工作负载。DuckDB 数据库格式中的默认行组大小为 122,880 行。并行处理从行组层面开始,因此,若要在一个查询中使用 k 个线程,它至少需要扫描 k * 122,880 行。

可以在 ATTACH 语句中将行组大小指定为一个选项。

ATTACH '/tmp/somefile.db' AS db (ROW_GROUP_SIZE 16384);

为 Parquet 文件选择 ROW_GROUP_SIZE 的性能考量完全适用于 DuckDB 自身的数据库格式。

线程过多

请注意,在某些情况下,DuckDB 可能会启动过多的线程(例如由于超线程技术),这可能导致处理速度变慢。在这种情况下,值得使用 SET threads = X 手动限制线程数量。

超内存工作负载(核外处理)

DuckDB 的一个关键优势是支持超内存工作负载,即它能够处理大于可用系统内存的数据集(也称为核外处理,out-of-core processing)。它还能运行中间结果无法放入内存的查询。本节解释了 DuckDB 中超内存处理的先决条件、适用范围和已知限制。

溢出到磁盘

超内存工作负载通过溢出到磁盘来支持。在默认配置下,DuckDB 会创建 database_file_name.tmp 临时目录(持久化模式下)或 .tmp 目录(内存模式下)。可以使用 temp_directory 配置选项更改此目录,例如:

SET temp_directory = '/path/to/temp_dir.tmp/';

阻塞算子

某些算子在看到输入的最后一行之前,无法输出任何一行。这些被称为阻塞算子,因为它们需要缓冲整个输入,是关系数据库系统中内存密集度最高的算子。主要的阻塞算子如下:

DuckDB 对上述所有算子均支持超内存处理。

限制

DuckDB 致力于在工作负载即使超过内存容量时也能完成任务。尽管如此,目前仍存在一些限制:

  • 如果查询中同时出现多个阻塞算子,由于这些算子之间复杂的相互作用,DuckDB 仍可能抛出内存不足异常。
  • 某些聚合函数(如 list()string_agg())不支持溢出到磁盘。
  • 使用排序的聚合函数是整体性的,即它们需要在开始聚合之前获取所有输入。由于 DuckDB 尚无法将一些复杂的中间聚合状态溢出到磁盘,因此这些函数在处理大型数据集时可能会导致内存不足异常。
  • PIVOT 操作在内部使用了 list() 函数,因此它也受到同样的限制。

性能分析

如果查询的性能不如预期,建议检查其查询计划。

  • 使用 EXPLAIN 查看物理查询计划,而无需实际执行查询。
  • 使用 EXPLAIN ANALYZE 运行并分析查询。这将显示查询中每一步所消耗的 CPU 时间。请注意,由于多线程的影响,各步骤时间的总和可能会大于查询的总处理时间。

查询计划可以指向性能问题的根源。以下是一些通用指导原则:

  • 避免使用嵌套循环连接,优先选择哈希连接。
  • 如果扫描操作未包含对过滤条件的下推(Filter Pushdown),且该条件随后才被应用,将会执行不必要的 IO。尝试重写查询以应用下推。
  • 应不惜一切代价避免算子基数(cardinality)爆炸至数十亿个元组的糟糕连接顺序。

预处理语句

预编译语句(Prepared statements)可以在多次运行相同查询但参数不同时提高性能。当语句被预编译时,它会完成查询执行过程的多个初始部分(解析、规划等)并缓存其输出。再次执行时,这些步骤可以跳过,从而提高性能。这对于频繁运行带有不同参数的小型查询(运行时 < 100ms)特别有效。

需要注意的是,快速并发执行大量小型查询并不是 DuckDB 的主要设计目标。相反,它针对运行大型、不频繁的查询进行了优化。

查询远程文件

DuckDB 在读取远程文件时使用同步 IO。这意味着每个 DuckDB 线程一次最多只能发出一个 HTTP 请求。如果查询必须通过网络发起大量小请求,将 DuckDB 的 threads 设置增加到超过 CPU 核心总数(约核心数的 2-5 倍)可以提高并行度和性能。

避免读取不必要的数据

读取远程文件的工作负载的主要瓶颈很可能是 IO。这意味着尽可能减少不必要的数据读取将非常有益。

一些基本的 SQL 技巧可以有所帮助:

  • 避免使用 SELECT *。只选择实际用到的列。DuckDB 将会尝试只下载它实际需要的数据。
  • 尽可能在远程 Parquet 文件上应用过滤条件。DuckDB 可以利用这些过滤器来减少需要扫描的数据量。
  • 根据经常用于过滤的列对数据进行排序分区:这可以增加过滤器在减少 IO 方面的有效性。

要检查一个查询传输了多少远程数据,可以使用 EXPLAIN ANALYZE 来打印远程文件查询的总请求数和传输的总数据量。

缓存

从 1.3.0 版本开始,DuckDB 支持缓存远程数据。要查看外部文件缓存的内容,请运行:

FROM duckdb_external_file_cache();

使用连接的最佳实践

当多次重用同一个数据库连接时,DuckDB 的表现最好。在每次查询时断开并重新连接会产生一定的开销,这在运行大量小型查询时会降低性能。DuckDB 也会在内存中缓存一些数据和元数据,当最后一个打开的连接关闭时,这些缓存会丢失。通常情况下,单个连接效果最好,但也可以使用连接池。

使用多个连接可以并行处理某些操作,尽管这通常是不必要的。DuckDB 确实尝试在每个单独的查询内尽可能多地进行并行化,但并非在所有情况下都能实现。建立多个连接可以同时处理更多操作。如果 DuckDB 不受 CPU 限制,而是受到网络传输速度等其他资源的瓶颈限制,这种方法会更有帮助。

持久化表与内存表

DuckDB 支持轻量级压缩技术。默认情况下,压缩仅应用于持久化(磁盘)数据库,而不应用于内存表。

在某些情况下,这可能会导致反直觉的性能结果:查询在磁盘表上比在内存表上更快。以 SF30 数据集上的 TPC-H 工作负载 Q1 为例:

CALL dbgen(sf = 30);
.timer on
PRAGMA tpch(1);

我们使用三个 DuckDB 提示符运行此脚本:

数据库设置 DuckDB 提示符 执行时间
内存数据库(未压缩) duckdb 4.22 秒
内存数据库(压缩) duckdb -cmd "ATTACH ':memory:' AS db (COMPRESS); USE db;" 0.55 秒
持久化数据库(压缩) duckdb tpch-sf30.db 0.56 秒

我们可以观察到,压缩后的数据库比未压缩的内存数据库快约 8 倍。

© 2025 DuckDB 基金会,阿姆斯特丹,荷兰
行为准则 商标使用指南