- 安装
- 文档
- 入门
- 连接
- 数据导入
- 客户端 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
- PIVOT
- 性能分析
- SELECT
- SET / RESET
- SET VARIABLE
- 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
- MySQL
- PostgreSQL
- 空间
- SQLite
- TPC-DS
- TPC-H
- UI
- 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
- 行为准则
- 发布日历
- 路线图
- 站点地图
- 在线演示
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/';
阻塞操作符
某些操作符在看到其输入的最后一行之前无法输出单行。这些被称为阻塞操作符,因为它们需要缓冲整个输入,并且是关系数据库系统中内存消耗最大的操作符。主要的阻塞操作符如下:
- 分组:
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)暴增到数十亿个元组。
预处理语句
预处理语句可以在多次运行相同查询但参数不同时提高性能。当语句被预处理时,它会完成查询执行过程的几个初始部分(解析、计划等)并缓存其输出。当它被执行时,这些步骤可以跳过,从而提高性能。这主要有利于重复运行带有不同参数集的小型查询(运行时 < 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_a
和 col_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_a
和 col_b
列复制到本地表中,然后两次查询本地内存中的列。另请注意,过滤器 WHERE col_a > 10
现在也只应用一次。
不过,这里需要做个重要的旁注。前两个查询是完全流式的,只占用少量内存,而第二个查询则需要完全物化 col_a
和 col_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 秒 |