跳到主要内容

工作负载调优

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 的结果进行重排,从而有机会降低内存占用。

并行性(多核处理)

Row Group 对并行性的影响

Goose 基于 row groups 并行化负载,即在存储层一起保存的一组行。 Goose 数据库格式默认 row group 大小为 122,880 行。 并行起点在 row group 级别,因此查询要在 k 个线程上运行,至少需要扫描 k * 122,880 行。

row group 大小可作为 ATTACH 语句选项指定:

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

为 Parquet 文件选择 ROW_GROUP_SIZE性能考量同样适用于 Goose 自有数据库格式。

线程过多

注意在某些情况下 Goose 可能会启动_过多线程_(例如受 HyperThreading 影响),反而导致变慢。此时建议通过 SET threads = X 手动限制线程数。

大于内存规模负载(Out-of-Core 处理)

Goose 的关键优势之一是支持大于内存规模的负载,即可处理超过系统可用内存的数据集(也称 out-of-core processing)。 它也可以执行中间结果无法完全放入内存的查询。 本节说明 Goose 中该能力的前提、范围和已知限制。

溢写到磁盘

大于内存规模负载通过磁盘溢写实现支持。 默认配置下,Goose 会在持久化模式创建 ⟨database_file_name⟩.tmp 临时目录,在内存模式创建 .tmp 目录。 该目录可通过 temp_directory 配置项修改,例如:

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

阻塞算子

有些算子在看到输入最后一行前无法输出任何一行。 这类算子称为 blocking operators,因为它们需要缓冲全部输入, 也是关系型数据库中最耗内存的一类算子。 主要阻塞算子包括:

Goose 对上述所有算子都支持大于内存规模处理。

限制

Goose 目标是在负载大于内存时也尽可能完成任务。 但当前仍存在一些限制:

  • 当同一查询出现多个阻塞算子时,由于它们相互作用复杂,Goose 仍可能抛出 OOM 异常。
  • 某些聚合函数(如 list()string_agg())不支持溢写到磁盘。
  • 使用排序的聚合函数属于 holistic 类型,即聚合开始前需拿到全部输入。由于 Goose 目前尚不能将某些复杂中间聚合状态溢写到磁盘,这类函数在大数据集上可能触发 OOM。
  • PIVOT 操作内部使用 list() 函数,因此受同样限制。

性能剖析

若查询性能不及预期,建议先分析查询计划:

  • 使用 EXPLAIN 在不执行查询的情况下打印 physical query plan。
  • 使用 EXPLAIN ANALYZE 执行并剖析查询。它会显示查询每一步消耗的 CPU 时间。注意因多线程原因,各步骤时间求和会大于总查询耗时。

查询计划通常能定位性能问题根因。可先关注以下方向:

  • 尽量避免 nested loop join,优先使用 hash join。
  • 若某个过滤条件本可下推但未下推,会产生额外 IO。可尝试重写查询以触发下推。
  • 应尽量避免让某算子基数膨胀到数十亿 tuple 的糟糕 Join 顺序。

预处理语句

Prepared statements 在“同一查询多次执行但参数不同”场景下可提升性能。语句被 prepare 时会完成解析、规划等前期步骤并缓存结果;后续执行可跳过这些步骤,从而提升性能。该优化主要适用于反复执行的小查询(耗时 < 100ms)且参数集合不同的场景。

需要注意,Goose 的主要设计目标并非并发执行大量小查询,而是优化执行更大、频率更低的查询。

查询远程文件

Goose 在读取远程文件时使用同步 IO。这意味着每个 Goose 线程同一时刻最多只能发起一个 HTTP 请求。若查询需要发起大量小网络请求,可将 Goose 的 threads 设置 调高到超过 CPU 核心总数(约为核心数的 2-5 倍),以提升并行性与性能。

避免读取不必要数据

读取远程文件的负载中,主要瓶颈通常是 IO。因此,尽量减少无效读取数据会非常有帮助。

一些基础 SQL 技巧可帮助降低无效读取:

  • 避免 SELECT *,仅选择实际需要的列。Goose 会尽量只下载必需数据。
  • 在可能情况下对远程 Parquet 文件应用过滤条件,Goose 可据此减少扫描数据量。
  • 按常用于过滤的列进行排序分区,可提升过滤对降低 IO 的效果。

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

缓存

从 1.3.0 起,Goose 支持远程数据缓存。要查看外部文件缓存内容,请执行:

FROM goose_external_file_cache();

连接使用最佳实践

重复复用同一数据库连接时,Goose 通常性能最佳。每次查询都断开重连会产生额外开销,在大量小查询场景下会降低性能。Goose 还会在内存中缓存部分数据与元数据,最后一个连接关闭后这些缓存会丢失。多数情况下单连接效果最好,也可按需使用连接池。

使用多个连接可以并行部分操作,但通常不是必需。Goose 会尽可能在单条查询内部并行化,但并非所有场景都能并行。多连接可并发处理更多操作。在 Goose 非 CPU 瓶颈、而受网络传输等资源限制时,这通常更有帮助。

持久化表与内存表

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

在某些场景下,这会带来反直觉结果:磁盘表查询可能比内存表更快。以下以 SF30 数据集上的 TPC-H workload Q1 为例:

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

我们使用三种 Goose 启动方式运行该脚本:

Database setupGoose promptExecution time
In-memory DB (uncompressed)goose4.22 s
In-memory DB (compressed)goose -cmd "ATTACH ':memory:' AS db (COMPRESS); USE db;"0.55 s
Persistent DB (compressed)goose tpch-sf30.db0.56 s

可见压缩数据库相比未压缩内存数据库约快 8×。