DuckDB 完全指南
DuckDB 是一个嵌入式列式 OLAP 数据库,无需独立服务进程,像 SQLite 一样嵌入应用内部运行,但专门针对分析型查询(大量数据扫描、聚合、连接)进行了优化。 与主要替代方案的对比: 适合的场景: 不适合的场景: DuckDB 有三种连接模式: duckdb.sql() 等模块级函数使用一个全局默认连接,等效于 duckdb.connect(":default:")。单文件脚本直接用模块级函数即可;需要隔离或多线程时才创建独立连接。 duckdb.sql() 等方法返回 DuckDBPyRelation,它是一个惰性求值的符号查询节点——不会立
官方文档:https://duckdb.org/docs/
适用版本:DuckDB 1.5.2(2026-05-07 核实)
概述
DuckDB 是一个嵌入式列式 OLAP 数据库,无需独立服务进程,像 SQLite 一样嵌入应用内部运行,但专门针对分析型查询(大量数据扫描、聚合、连接)进行了优化。
与主要替代方案的对比:
| DuckDB | SQLite | ClickHouse | Pandas | |
|---|---|---|---|---|
| 部署方式 | 嵌入式,无服务 | 嵌入式,无服务 | 独立服务 | 纯内存 |
| 查询引擎 | 列式 OLAP | 行式 OLTP | 列式 OLAP | 向量化 |
| 适合数据量 | GB 级至 TB 级 | MB 至低 GB 级 | TB 至 PB 级 | GB 级(受内存限制) |
| SQL 兼容性 | 完整 SQL | 部分 SQL | ClickHouse 方言 | 无 SQL |
| 读取 Parquet/CSV | 原生支持 | 需插件 | 原生支持 | 需 read_csv/read_parquet |
适合的场景:
- 本地数据分析、数据探索(替代 Pandas 处理大文件)
- ETL 管道中的中间层处理
- 读取并分析 Parquet / CSV / JSON 文件,无需数据库服务器
- 需要 SQL 但不想维护 ClickHouse 服务的场景
- Jupyter Notebook 数据分析工作流
不适合的场景:
- 高并发 OLTP 写入(DuckDB 针对分析读,不是高频写)
- 需要多进程并发写入同一数据库文件
- 需要实时流处理(考虑 Kafka + ClickHouse)
安装
# 基础安装
pip install duckdb
# 含 Pandas 集成
pip install duckdb pandas
# 含 Polars 集成
pip install duckdb polars
# 含 PyArrow 集成
pip install duckdb pyarrow
核心概念
连接模式
DuckDB 有三种连接模式:
| 模式 | 写法 | 说明 |
|---|---|---|
| 内存数据库 | duckdb.connect() 或 duckdb.connect(":memory:") |
进程退出后数据消失,最快 |
| 命名内存数据库 | duckdb.connect(":memory:mydb") |
同进程内多个连接共享同一 catalog |
| 持久化文件 | duckdb.connect("data.db") |
数据持久化到磁盘 |
模块级默认连接
duckdb.sql() 等模块级函数使用一个全局默认连接,等效于 duckdb.connect(":default:")。单文件脚本直接用模块级函数即可;需要隔离或多线程时才创建独立连接。
Relation(关系对象)
duckdb.sql() 等方法返回 DuckDBPyRelation,它是一个惰性求值的符号查询节点——不会立即执行查询,链式调用 .filter()、.limit() 等方法都只是修改查询计划。调用 .fetchall()、.to_df() 等输出方法时才真正执行。
API 详解
duckdb.connect()
创建数据库连接。
import duckdb
con = duckdb.connect(database=":memory:", read_only=False, config={})
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
database |
str |
":memory:" |
数据库路径;":memory:" 内存库;":memory:name" 命名内存库;文件路径则持久化 |
read_only |
bool |
False |
以只读模式打开,允许多进程同时读取同一文件 |
config |
dict |
{} |
数据库配置键值对,如 {"threads": 4, "memory_limit": "4GB"} |
config 常用选项:
| 键 | 类型 | 默认值 | 说明 |
|---|---|---|---|
threads |
int |
CPU 核心数 | 并行线程数 |
memory_limit |
str |
"80%" 物理内存 |
最大内存用量,如 "4GB" |
temp_directory |
str |
系统临时目录 | 超出内存时溢写磁盘的目录 |
max_memory |
str |
同 memory_limit |
memory_limit 的别名 |
storage_compatibility_version |
str |
"latest" |
写入兼容旧版的数据库文件时使用 |
# 生产环境:限制资源
con = duckdb.connect(
database="analytics.db",
config={
"threads": 8,
"memory_limit": "16GB",
"temp_directory": "/tmp/duckdb",
}
)
Connection — 查询执行方法
execute()
执行单条 SQL 语句,支持参数化查询。
con.execute(query, parameters=None)
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
query |
str |
必填 | SQL 语句,支持 ? 位置占位符或 $1、$name 命名占位符 |
parameters |
list | tuple | None |
None |
参数列表,与占位符对应 |
返回值:DuckDBPyConnection(支持链式调用)
# 位置参数:使用 ? 占位符
con.execute(
"INSERT INTO users (name, age) VALUES (?, ?)",
["Alice", 30]
)
# 命名参数:$name 格式
con.execute(
"SELECT * FROM users WHERE name = $name",
{"name": "Alice"}
)
# 链式:execute 后立即 fetchall
rows = con.execute("SELECT * FROM users LIMIT 10").fetchall()
executemany()
批量执行,对每组参数执行一次 SQL。
con.executemany(query, parameter_sequence)
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
query |
str |
必填 | 含占位符的 SQL 语句 |
parameter_sequence |
list[list] | list[tuple] |
必填 | 每次调用的参数列表 |
users = [("Bob", 25), ("Carol", 35), ("Dave", 28)]
con.executemany("INSERT INTO users VALUES (?, ?)", users)
大批量插入应优先使用 INSERT INTO ... SELECT 或 COPY FROM,executemany 性能远低于批量 SQL。
Connection — 结果获取方法
fetchone()
返回下一行,无更多结果时返回 None。
row = con.execute("SELECT name FROM users").fetchone()
# row = ("Alice",) 或 None
fetchmany()
返回指定数量的行。
con.fetchmany(size=1)
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
size |
int |
1 |
最多返回的行数;实际行数可能少于此值 |
fetchall()
返回所有剩余行,结果为 list[tuple]。
rows = con.execute("SELECT * FROM users").fetchall()
# [("Alice", 30), ("Bob", 25), ...]
fetchdf()
返回 pandas DataFrame。需要安装 pandas。
df = con.execute("SELECT * FROM users").fetchdf()
# 等价于 .to_df()
fetchnumpy()
返回 numpy 数组字典 dict[str, np.ndarray],键为列名。
arrays = con.execute("SELECT age FROM users").fetchnumpy()
# {"age": array([30, 25, 35])}
pl()
返回 Polars DataFrame。需要安装 polars。
df = con.execute("SELECT * FROM users").pl()
arrow()
返回 PyArrow Table。需要安装 pyarrow。
tbl = con.execute("SELECT * FROM users").arrow()
Connection — 连接管理方法
cursor()
在同一底层连接上创建新的游标(Cursor),可用于并发查询。
cursor = con.cursor()
cursor.execute("SELECT ...")
close()
显式关闭连接,释放资源。通常通过上下文管理器自动关闭。
# 推荐:使用上下文管理器
with duckdb.connect("data.db") as con:
result = con.execute("SELECT 42").fetchone()
# 超出 with 块后自动关闭
register() / unregister()
将 Python 对象(DataFrame、Arrow Table 等)注册为 DuckDB 虚拟视图。
con.register(view_name, python_object)
con.unregister(view_name)
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
view_name |
str |
必填 | 在 SQL 中使用的视图名称 |
python_object |
DataFrame | Table | ... |
必填 | pandas DataFrame、Polars DataFrame 或 PyArrow Table |
import pandas as pd
df = pd.read_csv("data.csv")
con.register("df_view", df)
result = con.execute("SELECT count(*) FROM df_view").fetchone()
con.unregister("df_view")
Relation API
Relation API 通过链式调用构建惰性查询,适合复杂的数据处理管道。
创建 Relation
duckdb.sql() / con.sql()
执行 SELECT SQL 并返回 Relation。
duckdb.sql(query, alias='', params=None)
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
query |
str |
必填 | SELECT 语句;非 SELECT 语句直接执行不返回 Relation |
alias |
str |
"" |
赋予 Relation 的别名,用于后续 SQL 引用 |
params |
list | None |
None |
查询参数;有性能开销,大批量场景建议避免 |
import duckdb
rel = duckdb.sql("SELECT * FROM 'data.parquet' WHERE year = 2024")
print(rel.columns) # 列名列表
print(rel.dtypes) # 列类型列表
duckdb.table() / con.table()
从已有表或视图创建 Relation。
duckdb.table(table_name)
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
table_name |
str |
必填 | 表名或视图名,支持 schema.table 格式 |
duckdb.from_df() / con.from_df()
从 pandas DataFrame 创建 Relation(零拷贝)。
duckdb.from_df(df)
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
df |
pandas.DataFrame |
必填 | 源 DataFrame,DuckDB 直接引用内存,不复制数据 |
duckdb.from_arrow() / con.from_arrow()
从 PyArrow Table 或 RecordBatch 创建 Relation(零拷贝)。
duckdb.from_arrow(arrow_object)
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
arrow_object |
pyarrow.Table | RecordBatch |
必填 | 源 Arrow 对象 |
Relation 转换方法(惰性求值)
filter() — WHERE 过滤
rel.filter(filter_expr)
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
filter_expr |
str |
必填 | SQL WHERE 表达式,不含 WHERE 关键字 |
rel = duckdb.sql("FROM sales").filter("amount > 1000 AND region = 'CN'")
project() / select() — SELECT 投影
rel.project(project_expr)
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
project_expr |
str |
必填 | 逗号分隔的列表达式,支持别名和表达式 |
rel = duckdb.sql("FROM users").project("name, age, age * 2 AS double_age")
order() / sort() — ORDER BY
rel.order(order_expr)
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
order_expr |
str |
必填 | 排序表达式,支持 ASC/DESC、NULLS FIRST/LAST |
rel = rel.order("amount DESC NULLS LAST, created_at ASC")
limit() — LIMIT / OFFSET
rel.limit(n, offset=0)
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
n |
int |
必填 | 最多返回的行数 |
offset |
int |
0 |
跳过的起始行数,用于分页 |
aggregate() — GROUP BY 聚合
rel.aggregate(aggr_expr, group_expr='')
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
aggr_expr |
str |
必填 | 聚合列表达式,如 "count(*), sum(amount)" |
group_expr |
str |
"" |
GROUP BY 列,如 "region, year" |
rel = (
duckdb.sql("FROM sales")
.aggregate("region, sum(amount) AS total", "region")
.order("total DESC")
)
join() — JOIN
rel.join(other_rel, condition, how='inner')
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
other_rel |
DuckDBPyRelation |
必填 | 右侧 Relation |
condition |
str |
必填 | JOIN 条件,不含 ON 关键字 |
how |
str |
"inner" |
连接类型:inner、left、right、outer、semi、anti |
orders = duckdb.table("orders")
users = duckdb.table("users")
result = orders.join(users, "orders.user_id = users.id", how="left")
集合操作
rel.union(other_rel) # UNION ALL
rel.intersect(other_rel) # INTERSECT
rel.except_(other_rel) # EXCEPT
rel.cross(other_rel) # CROSS JOIN
Relation 输出方法(触发执行)
to_df() — 输出为 pandas DataFrame
df = rel.to_df()
to_arrow() — 输出为 PyArrow Table
tbl = rel.to_arrow()
to_csv() — 写出 CSV 文件
rel.to_csv(filename, **kwargs)
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
filename |
str |
必填 | 输出文件路径 |
sep |
str |
"," |
列分隔符 |
header |
bool |
True |
是否写列名 |
quotechar |
str |
'"' |
引号字符 |
compression |
str |
"auto" |
压缩方式:none、gzip、zstd |
to_parquet() — 写出 Parquet 文件
rel.to_parquet(filename, **kwargs)
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
filename |
str |
必填 | 输出文件路径 |
compression |
str |
"snappy" |
压缩算法:snappy、zstd、gzip、lz4、uncompressed |
row_group_size |
int |
122880 |
每个 row group 的行数,影响读取性能 |
show() — 打印结果表格
rel.show(max_rows=None, max_width=None)
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
max_rows |
int | None |
None(使用全局设置) |
最多显示的行数 |
max_width |
int | None |
None(使用全局设置) |
最大显示宽度(字符数) |
describe() — 描述性统计
rel.describe() # 返回 count / mean / std / min / max / median
explain() — 查询计划
rel.explain(type='standard')
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
type |
str |
"standard" |
计划类型:standard(逻辑计划)、analyze(含实际行数统计) |
数据导入:read_csv()
直接读取 CSV 文件,可用于 SQL 中或作为 Relation 起点。
-- SQL 中直接读取
SELECT * FROM read_csv('data.csv', header=true, delim=',');
-- 读取目录下所有 CSV
SELECT * FROM read_csv('data/*.csv', union_by_name=true);
# Python Relation API
rel = duckdb.read_csv("data.csv", header=True)
完整参数表:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
all_varchar |
BOOL |
false |
跳过类型推断,所有列视为 VARCHAR |
allow_quoted_nulls |
BOOL |
true |
允许带引号的 NULL 值 |
auto_detect |
BOOL |
true |
自动推断分隔符、列名、类型等 |
auto_type_candidates |
TYPE[] |
全类型 | 类型推断时的候选类型列表 |
buffer_size |
BIGINT |
16 * max_line_size |
读取缓冲区大小(字节) |
columns |
STRUCT |
空 | 显式指定列名和类型,格式 {'col': 'VARCHAR'} |
comment |
VARCHAR |
"" |
注释起始字符,该字符开头的行被跳过 |
compression |
VARCHAR |
"auto" |
压缩格式:auto、none、gzip、zstd |
dateformat |
VARCHAR |
"" |
日期解析格式,如 %Y-%m-%d |
decimal_separator |
VARCHAR |
"." |
数值的小数点字符 |
delim / sep |
VARCHAR |
"," |
列分隔符,最多 4 字节 |
encoding |
VARCHAR |
"utf-8" |
文件编码:utf-8、utf-16、latin-1 |
escape |
VARCHAR |
'"' |
引号内的转义字符 |
filename |
BOOL |
false |
在结果中添加 filename 虚拟列 |
force_not_null |
VARCHAR[] |
[] |
指定列中空字符串不转为 NULL |
header |
BOOL |
false |
第一行是否为列名 |
hive_partitioning |
BOOL |
自动检测 | 按 Hive 分区格式(key=value/)解析路径 |
ignore_errors |
BOOL |
false |
解析错误时跳过该行而非报错 |
max_line_size |
BIGINT |
2000000 |
单行最大字节数 |
names / column_names |
VARCHAR[] |
空 | 列名列表,覆盖文件中的列名 |
new_line |
VARCHAR |
"" |
换行符:\r、\n、\r\n |
normalize_names |
BOOL |
false |
将列名中非字母数字字符替换为下划线 |
null_padding |
BOOL |
false |
列数不足时用 NULL 填充缺失列 |
nullstr / null |
VARCHAR | VARCHAR[] |
"" |
代表 NULL 的字符串,如 "N/A" |
parallel |
BOOL |
true |
启用并行 CSV 读取 |
quote |
VARCHAR |
'"' |
引号字符 |
sample_size |
BIGINT |
20480 |
自动检测时采样的行数 |
skip |
BIGINT |
0 |
跳过文件开头的行数 |
store_rejects |
BOOL |
false |
将解析错误行存入 rejects_table 而非报错 |
strict_mode |
BOOL |
true |
严格模式:true 遇错误立即抛出,false 尝试继续解析 |
timestampformat |
VARCHAR |
"" |
时间戳解析格式 |
types / dtypes |
VARCHAR[] | STRUCT |
空 | 按位置或列名指定列类型 |
union_by_name |
BOOL |
false |
合并多个文件时按列名对齐(而非按位置) |
数据导入:read_parquet()
直接读取 Parquet 文件,支持 glob 通配符和 Hive 分区。
-- 读取单文件
SELECT * FROM read_parquet('data.parquet');
-- 读取目录下所有 Parquet(Hive 分区)
SELECT * FROM read_parquet('data/year=*/month=*/*.parquet', hive_partitioning=true);
rel = duckdb.read_parquet("data/year=2024/**/*.parquet", hive_partitioning=True)
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
binary_as_string |
BOOL |
false |
将 BINARY 类型列读取为字符串(兼容旧格式写入的文件) |
encryption_config |
STRUCT |
- | 加密 Parquet 文件的解密配置 |
filename |
BOOL |
false |
结果中添加文件路径列(v1.3.0+ 自动包含) |
file_row_number |
BOOL |
false |
结果中添加文件内行号列 |
hive_partitioning |
BOOL |
自动检测 | 将路径中的 key=value 目录解析为列 |
union_by_name |
BOOL |
false |
多文件 Schema 不同时,按列名而非位置合并 |
schema |
MAP |
NULL |
强制使用指定 Schema 读取,需配合字段 ID |
数据导出:COPY TO
导出 CSV
COPY (SELECT * FROM sales WHERE year = 2024)
TO 'output.csv'
(FORMAT CSV, HEADER true, DELIMITER ',', COMPRESSION gzip);
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
FORMAT |
VARCHAR |
- | 文件格式:CSV、PARQUET、JSON |
HEADER |
BOOL |
true |
是否写入列名行 |
DELIMITER / DELIM / SEP |
VARCHAR |
"," |
列分隔符 |
QUOTE |
VARCHAR |
'"' |
引号字符 |
ESCAPE |
VARCHAR |
'"' |
引号内的转义字符 |
NULLSTR |
VARCHAR |
"" |
NULL 值写出的字符串 |
DATEFORMAT |
VARCHAR |
"" |
日期格式 |
TIMESTAMPFORMAT |
VARCHAR |
"" |
时间戳格式 |
COMPRESSION |
VARCHAR |
"auto" |
压缩:none、gzip、zstd |
FORCE_QUOTE |
VARCHAR[] |
[] |
强制带引号的列 |
导出 Parquet
COPY (SELECT * FROM large_table)
TO 'output.parquet'
(FORMAT PARQUET, COMPRESSION zstd, COMPRESSION_LEVEL 3, ROW_GROUP_SIZE 100000);
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
COMPRESSION |
VARCHAR |
"snappy" |
压缩算法:snappy、zstd、gzip、lz4、uncompressed |
COMPRESSION_LEVEL |
BIGINT |
3 |
zstd 压缩级别(1–22),越大越小越慢 |
ROW_GROUP_SIZE |
BIGINT |
122880 |
每个 row group 的行数,影响读取并行度 |
ROW_GROUPS_PER_FILE |
BIGINT |
- | 每个文件最大 row group 数,超出则拆分新文件 |
PARQUET_VERSION |
VARCHAR |
"V1" |
Parquet 规范版本:V1 或 V2 |
FIELD_IDS |
STRUCT |
- | 自定义字段 ID(用于 Schema 演进) |
完整示例
场景一:本地 CSV 数据分析
import duckdb
# 读取并分析 CSV,无需加载到内存
result = duckdb.sql("""
SELECT
region,
count(*) AS order_count,
sum(amount) AS total_revenue,
avg(amount) AS avg_order
FROM read_csv('orders_2024.csv', header=true, auto_detect=true)
WHERE status = 'completed'
GROUP BY region
ORDER BY total_revenue DESC
""").to_df()
print(result.head(10))
场景二:处理多个 Parquet 分区文件
import duckdb
con = duckdb.connect()
# 读取 Hive 分区 Parquet,合并 Schema 不同的文件
rel = con.read_parquet(
"s3://bucket/data/year=*/month=*/*.parquet",
hive_partitioning=True,
union_by_name=True,
)
# 按分区聚合,过滤特定月份
monthly = (
rel
.filter("year = 2024")
.aggregate("year, month, sum(revenue) AS total", "year, month")
.order("year, month")
)
monthly.to_parquet("monthly_summary.parquet", compression="zstd")
场景三:与 Pandas 零拷贝协作
import duckdb
import pandas as pd
# 已有 Pandas DataFrame,直接交给 DuckDB 查询
df = pd.read_parquet("large_file.parquet") # 假设 Pandas 能加载
# 零拷贝:DuckDB 直接引用 df 的内存
result = duckdb.sql("""
SELECT
category,
percentile_cont(0.95) WITHIN GROUP (ORDER BY price) AS p95_price
FROM df
GROUP BY category
""").to_df()
场景四:ETL 管道:CSV → 清洗 → Parquet
import duckdb
with duckdb.connect("staging.db") as con:
# 建表并导入
con.execute("""
CREATE OR REPLACE TABLE raw_events AS
SELECT * FROM read_csv('events/*.csv',
header=true,
nullstr=['N/A', 'null', ''],
normalize_names=true
)
""")
# 清洗并导出
con.execute("""
COPY (
SELECT
event_id,
user_id,
strptime(event_time, '%Y-%m-%d %H:%M:%S') AS event_ts,
lower(trim(event_type)) AS event_type,
json_object('source', source, 'page', page) AS metadata
FROM raw_events
WHERE event_id IS NOT NULL
AND user_id > 0
)
TO 'clean_events.parquet'
(FORMAT PARQUET, COMPRESSION zstd, ROW_GROUP_SIZE 100000)
""")
print(f"Total rows: {con.execute('SELECT count(*) FROM raw_events').fetchone()[0]}")
场景五:持久化数据库的多步骤分析
import duckdb
with duckdb.connect("analytics.db") as con:
# 创建或替换表
con.execute("""
CREATE OR REPLACE TABLE sales AS
SELECT * FROM read_parquet('raw/*.parquet', hive_partitioning=true)
""")
# 创建视图
con.execute("""
CREATE OR REPLACE VIEW monthly_kpi AS
SELECT
date_trunc('month', sale_date) AS month,
sum(revenue) AS revenue,
count(DISTINCT customer_id) AS unique_customers,
sum(revenue) / count(DISTINCT customer_id) AS arpu
FROM sales
GROUP BY 1
""")
# 查询视图
df = con.sql("SELECT * FROM monthly_kpi ORDER BY month").to_df()
最佳实践
尽量直接查询文件而非先导入: DuckDB 能直接 SELECT Parquet/CSV 文件,对大文件只需要扫描不需要拷贝。当文件需要多次查询时再 CREATE TABLE AS SELECT 物化到数据库。
-- 对大型分析任务,先把需要的数据列和分区投影到 DuckDB 表
CREATE TABLE local_sales AS
SELECT region, product_id, amount, sale_date
FROM read_parquet('s3://bucket/sales/*.parquet', hive_partitioning=true)
WHERE sale_date >= '2024-01-01';
-- 之后的查询全部针对 local_sales,无需重复读 S3
使用上下文管理器管理连接: 用 with duckdb.connect(...) as con: 确保连接关闭,避免文件锁残留(持久化数据库文件只能有一个写入连接)。
# 正确
with duckdb.connect("data.db") as con:
con.execute("INSERT INTO ...")
# 错误:忘记关闭会导致下次打开失败(文件被锁)
con = duckdb.connect("data.db")
con.execute("INSERT INTO ...")
并行读取多文件用 glob 而非循环: DuckDB 内置并行文件读取,一个 glob 表达式比 Python 循环拼接快数倍。
# 推荐:glob,DuckDB 并行读取所有文件
duckdb.sql("SELECT * FROM read_parquet('data/year=2024/*.parquet')")
# 不推荐:Python 循环,串行读取
import glob
for f in glob.glob("data/year=2024/*.parquet"):
duckdb.sql(f"SELECT * FROM '{f}'")
参数化查询防止 SQL 注入: 动态构造查询时使用占位符,绝不拼接字符串。
user_id = request.args.get("user_id")
# 正确:参数化查询
result = con.execute(
"SELECT * FROM users WHERE id = ?",
[user_id]
).fetchall()
# 错误:字符串拼接,存在 SQL 注入风险
result = con.execute(f"SELECT * FROM users WHERE id = {user_id}").fetchall()
设置合理的内存上限: 默认 DuckDB 使用 80% 物理内存,在共享环境(CI、容器)中应显式限制。
con = duckdb.connect(config={
"memory_limit": "4GB",
"temp_directory": "/tmp/duckdb_spill", # 超出内存时溢写磁盘
"threads": 4,
})
大批量插入用 INSERT INTO ... SELECT 而非 executemany: executemany 每行单独执行,百万行级别比 bulk insert 慢 100 倍以上。
# 推荐:从 DataFrame 直接写入
duckdb.sql("INSERT INTO target_table SELECT * FROM df")
# 或注册后批量写
con.register("staging", df)
con.execute("INSERT INTO target_table SELECT * FROM staging")
# 不推荐:executemany 逐行插入
con.executemany("INSERT INTO target_table VALUES (?, ?, ?)", rows)
常见陷阱
陷阱:持久化数据库被多进程写锁阻塞
现象: 第二个进程打开同一个 .db 文件时抛出 IOException: Could not set lock on file,或程序卡住等待锁释放。
原因: DuckDB 持久化文件不支持多进程并发写入,同一时刻只能有一个写入连接。
解决: 多进程场景使用 read_only=True 只读连接(允许并发);写入操作集中在单一进程;或改用内存数据库并定期 checkpoint。
# 读进程:只读模式,允许并发
reader = duckdb.connect("analytics.db", read_only=True)
df = reader.sql("SELECT * FROM monthly_kpi").to_df()
# 写进程:独占写连接,完成后务必关闭
with duckdb.connect("analytics.db") as writer:
writer.execute("INSERT INTO ...")
陷阱:read_csv 类型推断错误导致数据丢失
现象: 某列被推断为 INTEGER 或 FLOAT,导致含字母的字符串值静默变为 NULL,或数值精度丢失。
原因: auto_detect=true 仅对前 sample_size(默认 20480)行采样,若前几万行全是数字、后面才出现字母,会推断错误类型。
解决: 显式指定有疑问的列类型,或加大 sample_size,或使用 all_varchar=true 先全读为字符串再转换。
# 错误:依赖自动推断
duckdb.sql("SELECT * FROM read_csv('data.csv', header=true)")
# 正确:显式指定关键列类型
duckdb.sql("""
SELECT * FROM read_csv('data.csv',
header=true,
columns={'id': 'VARCHAR', 'code': 'VARCHAR', 'amount': 'DECIMAL(18,4)'}
)
""")
# 或加大采样范围
duckdb.sql("""
SELECT * FROM read_csv('data.csv', header=true, sample_size=-1)
""") # -1 表示全量采样
陷阱:Relation 对象被垃圾回收后查询失败
现象: 将 from_df(df) 创建的 Relation 保存到变量,之后再查询时报 InvalidInputException 或结果为空。
原因: Relation 直接引用底层 Python 对象的内存(零拷贝)。如果原始 DataFrame df 被垃圾回收或被重新赋值,Relation 会访问已释放的内存。
解决: 确保原始对象的生命周期覆盖 Relation 的使用期,或在创建 Relation 后立即物化(调用 .to_df() 或 CREATE TABLE AS SELECT)。
def get_analysis():
df = pd.read_parquet("data.parquet")
rel = duckdb.from_df(df)
# 错误:df 在函数返回后会被 GC,rel 持有悬空引用
return rel # 调用方使用 rel 时可能崩溃
# 正确:在函数内物化
def get_analysis():
df = pd.read_parquet("data.parquet")
return duckdb.from_df(df).to_df() # 立即物化为新 DataFrame
陷阱:模块级默认连接在多线程中共享状态
现象: 多线程并发调用 duckdb.sql() 时出现查询结果混乱或报错。
原因: duckdb.sql() 使用全局默认连接,该连接不是线程安全的。
解决: 每个线程创建独立连接。
import threading
import duckdb
def worker(data):
# 正确:每个线程独立连接
with duckdb.connect() as con:
return con.sql(f"SELECT count(*) FROM data").fetchone()[0]
# 错误:多线程共享 duckdb.sql(默认连接)
threads = [threading.Thread(target=lambda: duckdb.sql("SELECT ...")) for _ in range(4)]
参见
- SQLite完全指南 — 同为嵌入式数据库,适合 OLTP 场景
- ClickHouse完全指南 — 服务端列式 OLAP,适合 TB 级以上数据
- Pandas完全指南 — DuckDB 常见协作对象,零拷贝互操作
- Polars完全指南 — 高性能 DataFrame,与 DuckDB 互操作性同样优秀
- 数据库设计规范 — 建表和索引设计原则