⌘+k ctrl+k
1.3 (稳定版)
搜索快捷键 cmd + k | ctrl + k
工作负载调优

preserve_insertion_order 选项

当导入或导出远大于可用内存的数据集(Parquet 或 CSV 格式)时,可能会发生内存不足错误。

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

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

SET preserve_insertion_order = false;

这允许系统重新排序任何不包含 ORDER BY 子句的结果,从而可能减少内存使用。

并行性(多核处理)

行组对并行性的影响

DuckDB 基于行组并行化工作负载,即在存储级别上一起存储的行组。DuckDB 数据库格式中的一个行组最多包含 122,880 行。并行性从行组级别开始,因此,查询若要在 k 个线程上运行,它需要扫描至少 k * 122,880 行。

线程过多

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

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

DuckDB 的一个主要优势是支持超内存工作负载,即它能够处理大于可用系统内存的数据集(也称为核外处理)。它还可以运行中间结果无法放入内存的查询。本节解释了 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)暴增到数十亿个元组。

预处理语句

预处理语句可以在多次运行相同查询但参数不同时提高性能。当语句被预处理时,它会完成查询执行过程的几个初始部分(解析、计划等)并缓存其输出。当它被执行时,这些步骤可以跳过,从而提高性能。这主要有利于重复运行带有不同参数集的小型查询(运行时 < 100ms)。

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

查询远程文件

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

避免读取不必要的数据

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

一些基本的 SQL 技巧可以帮助解决这个问题:

  • 避免使用 SELECT *。相反,只选择实际使用的列。DuckDB 会尝试只下载它实际需要的数据。
  • 如果可能,在远程 Parquet 文件上应用过滤器。DuckDB 可以使用这些过滤器来减少扫描的数据量。
  • 根据经常用于过滤的列对数据进行排序分区:这会提高过滤器在减少 IO 方面的效率。

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

避免多次读取数据

DuckDB 不会自动缓存远程文件中的数据。这意味着对同一远程文件运行两次查询将下载两次所需数据。因此,如果数据需要多次访问,将其本地存储可能更有意义。为了说明这一点,我们来看一个示例:

考虑以下查询:

SELECT col_a + col_b FROM 's3://bucket/file.parquet' WHERE col_a > 10;
SELECT col_a * col_b FROM 's3://bucket/file.parquet' WHERE col_a > 10;

这些查询会从 s3://bucket/file.parquet 下载 col_acol_b 列两次。现在考虑以下查询:

CREATE TABLE local_copy_of_file AS
    SELECT col_a, col_b FROM 's3://bucket/file.parquet' WHERE col_a > 10;

SELECT col_a + col_b FROM local_copy_of_file;
SELECT col_a * col_b FROM local_copy_of_file;

这里 DuckDB 会首先将 s3://bucket/file.parquet 中的 col_acol_b 列复制到本地表中,然后两次查询本地内存中的列。另请注意,过滤器 WHERE col_a > 10 现在也只应用一次。

不过,这里需要做个重要的旁注。前两个查询是完全流式的,只占用少量内存,而第二个查询则需要完全物化 col_acol_b 列。这意味着在某些罕见情况下(例如,高速网络,但可用内存非常有限),下载两次数据实际上可能更有益。

使用连接的最佳实践

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

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

持久表与内存表

DuckDB 支持轻量级压缩技术。目前,这些技术仅应用于持久(磁盘)数据库。

DuckDB 不压缩其内存表。原因是压缩是在检查点过程中执行的,而内存表不会进行检查点。

在某些情况下,这可能导致反直觉的性能结果,即磁盘上的表查询速度比内存中的表更快。例如,TPC-H 工作负载的 Q1 在磁盘上运行时比在内存模式下更快:

INSTALL tpch;
LOAD tpch;
CALL dbgen(sf = 30);
.timer on
PRAGMA tpch(1);
数据库设置 执行时间
内存数据库 4.80 秒
持久化数据库 0.57 秒