- 安装
- 文档
- 入门
- 连接
- 数据导入与导出
- 湖仓格式
- 客户端 API
- 概览
- 第三方客户端
- ADBC
- C
- C++
- CLI
- Dart
- Go
- Java (JDBC)
- Julia
- Node.js (已弃用)
- Node.js (Neo)
- ODBC
- PHP
- Python
- R
- Rust
- Swift
- Wasm
- SQL
- 介绍
- 语句
- 概览
- ANALYZE
- ALTER TABLE
- ALTER VIEW
- ATTACH 和 DETACH
- CALL
- CHECKPOINT
- COMMENT ON
- COPY
- CREATE INDEX
- CREATE MACRO
- CREATE SCHEMA
- CREATE SECRET
- CREATE SEQUENCE
- CREATE TABLE
- CREATE VIEW
- CREATE TYPE
- DELETE
- DESCRIBE
- DROP
- EXPORT 和 IMPORT DATABASE
- INSERT
- LOAD / INSTALL
- MERGE INTO
- PIVOT
- 性能分析
- SELECT
- SET / RESET
- SET VARIABLE
- SHOW 与 SHOW DATABASES
- SUMMARIZE
- 事务管理
- UNPIVOT
- UPDATE
- USE
- VACUUM
- 查询语法
- SELECT
- FROM 和 JOIN
- WHERE
- GROUP BY
- GROUPING SETS
- HAVING
- ORDER BY
- LIMIT 和 OFFSET
- SAMPLE
- 展开嵌套
- WITH
- WINDOW
- QUALIFY
- VALUES
- FILTER
- 集合操作
- 预处理语句
- 数据类型
- 表达式
- 函数
- 概览
- 聚合函数
- 数组函数
- 位字符串函数
- Blob 函数
- 日期格式化函数
- 日期函数
- 日期部分函数
- 枚举函数
- 间隔函数
- Lambda 函数
- 列表函数
- 映射函数
- 嵌套函数
- 数值函数
- 模式匹配
- 正则表达式
- 结构体函数
- 文本函数
- 时间函数
- 时间戳函数
- 带时区时间戳函数
- 联合函数
- 实用函数
- 窗口函数
- 约束
- 索引
- 元查询
- DuckDB 的 SQL 方言
- 示例
- 配置
- 扩展
- 核心扩展
- 概览
- 自动补全
- Avro
- AWS
- Azure
- Delta
- DuckLake
- 编码
- Excel
- 全文搜索
- httpfs (HTTP 和 S3)
- Iceberg
- ICU
- inet
- jemalloc
- Lance
- MySQL
- PostgreSQL
- 空间
- SQLite
- TPC-DS
- TPC-H
- UI
- Unity Catalog
- Vortex
- VSS
- 指南
- 概览
- 数据查看器
- 数据库集成
- 文件格式
- 概览
- CSV 导入
- CSV 导出
- 直接读取文件
- Excel 导入
- Excel 导出
- JSON 导入
- JSON 导出
- Parquet 导入
- Parquet 导出
- 查询 Parquet 文件
- 使用 file: 协议访问文件
- 网络和云存储
- 概览
- HTTP Parquet 导入
- S3 Parquet 导入
- S3 Parquet 导出
- S3 Iceberg 导入
- S3 Express One
- GCS 导入
- Cloudflare R2 导入
- 通过 HTTPS / S3 使用 DuckDB
- Fastly 对象存储导入
- 元查询
- ODBC
- 性能
- Python
- 安装
- 执行 SQL
- Jupyter Notebooks
- marimo Notebooks
- Pandas 上的 SQL
- 从 Pandas 导入
- 导出到 Pandas
- 从 Numpy 导入
- 导出到 Numpy
- Arrow 上的 SQL
- 从 Arrow 导入
- 导出到 Arrow
- Pandas 上的关系型 API
- 多个 Python 线程
- 与 Ibis 集成
- 与 Polars 集成
- 使用 fsspec 文件系统
- SQL 编辑器
- SQL 功能
- 代码片段
- 故障排除
- 术语表
- 离线浏览
- 操作手册
- 概览
- DuckDB 的占用空间
- 安装 DuckDB
- 日志
- 保护 DuckDB 安全
- 非确定性行为
- 限制
- DuckDB Docker 容器
- 开发
- 内部结构
- 站点地图
- 在线演示
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/';
阻塞算子
某些算子在看到输入的最后一行之前,无法输出任何一行。这些被称为阻塞算子,因为它们需要缓冲整个输入,是关系数据库系统中内存密集度最高的算子。主要的阻塞算子如下:
- 分组:
GROUP BY - 连接:
JOIN - 排序:
ORDER BY - 窗口函数:
OVER ... (PARTITION BY ... ORDER BY ...)
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 倍。